@kage-core/kage-graph-mcp 1.1.17 → 1.1.18
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 +13 -0
- package/dist/kernel.js +460 -88
- package/package.json +1 -1
- package/viewer/app.js +92 -1
- package/viewer/index.html +2 -9
- package/viewer/styles.css +49 -0
package/README.md
CHANGED
|
@@ -9,6 +9,19 @@ This package exposes two surfaces:
|
|
|
9
9
|
|
|
10
10
|
## Latest Release
|
|
11
11
|
|
|
12
|
+
`1.1.18` publishes the end-to-end performance pass:
|
|
13
|
+
|
|
14
|
+
- read-only commands reuse current graph artifacts instead of rebuilding them
|
|
15
|
+
when inputs are fresh.
|
|
16
|
+
- MCP sessions keep an in-process graph cache, so repeated agent calls do not
|
|
17
|
+
keep reparsing the same graph JSON.
|
|
18
|
+
- `kage refresh` reports lightweight freshness metrics and leaves deep
|
|
19
|
+
benchmark/quality work to explicit `kage metrics` and `kage benchmark` calls.
|
|
20
|
+
- recall builds graph lookup maps once per query instead of scanning all graph
|
|
21
|
+
entities and edges for every memory packet.
|
|
22
|
+
- `kage init` remains a packet-only bootstrap path; full graph generation stays
|
|
23
|
+
with `kage refresh` and `kage index`.
|
|
24
|
+
|
|
12
25
|
`1.1.17` publishes content-based graph freshness:
|
|
13
26
|
|
|
14
27
|
- `kage pr check` now uses graph input hashes, so push-only operations and
|
package/dist/kernel.js
CHANGED
|
@@ -134,6 +134,7 @@ exports.MEMORY_TYPES = [
|
|
|
134
134
|
"negative_result",
|
|
135
135
|
"constraint",
|
|
136
136
|
];
|
|
137
|
+
const graphMemoryCache = new Map();
|
|
137
138
|
exports.SETUP_AGENTS = [
|
|
138
139
|
"codex",
|
|
139
140
|
"claude-code",
|
|
@@ -1297,6 +1298,11 @@ const CODE_EXTENSIONS = new Set([
|
|
|
1297
1298
|
".hpp",
|
|
1298
1299
|
".swift",
|
|
1299
1300
|
]);
|
|
1301
|
+
const MAX_CODE_FILE_BYTES = positiveIntEnv("KAGE_MAX_CODE_FILE_BYTES", 512 * 1024);
|
|
1302
|
+
const MAX_CODE_GRAPH_FILES = positiveIntEnv("KAGE_MAX_CODE_GRAPH_FILES", 2000);
|
|
1303
|
+
const MAX_CODE_GRAPH_SYMBOLS = positiveIntEnv("KAGE_MAX_CODE_GRAPH_SYMBOLS", 25000);
|
|
1304
|
+
const MAX_CODE_GRAPH_CALLS = positiveIntEnv("KAGE_MAX_CODE_GRAPH_CALLS", 50000);
|
|
1305
|
+
const MAX_CODE_GRAPH_CALLS_PER_FILE = positiveIntEnv("KAGE_MAX_CODE_GRAPH_CALLS_PER_FILE", 250);
|
|
1300
1306
|
const CONFIG_NAMES = new Set([
|
|
1301
1307
|
"package.json",
|
|
1302
1308
|
"pyproject.toml",
|
|
@@ -1315,6 +1321,10 @@ const CONFIG_NAMES = new Set([
|
|
|
1315
1321
|
"vitest.config.js",
|
|
1316
1322
|
"vitest.config.ts",
|
|
1317
1323
|
]);
|
|
1324
|
+
function positiveIntEnv(name, fallback) {
|
|
1325
|
+
const value = Number(process.env[name]);
|
|
1326
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
1327
|
+
}
|
|
1318
1328
|
function extensionOf(path) {
|
|
1319
1329
|
const match = path.match(/\.[^.\/]+$/);
|
|
1320
1330
|
return match ? match[0] : "";
|
|
@@ -1322,7 +1332,26 @@ function extensionOf(path) {
|
|
|
1322
1332
|
function shouldSkipCodePath(relativePath) {
|
|
1323
1333
|
return relativePath
|
|
1324
1334
|
.split("/")
|
|
1325
|
-
.some((part) => [
|
|
1335
|
+
.some((part) => [
|
|
1336
|
+
".git",
|
|
1337
|
+
".agent_memory",
|
|
1338
|
+
"node_modules",
|
|
1339
|
+
"vendor",
|
|
1340
|
+
".venv",
|
|
1341
|
+
"venv",
|
|
1342
|
+
"__pycache__",
|
|
1343
|
+
"dist",
|
|
1344
|
+
"build",
|
|
1345
|
+
"coverage",
|
|
1346
|
+
".next",
|
|
1347
|
+
".nuxt",
|
|
1348
|
+
".output",
|
|
1349
|
+
".turbo",
|
|
1350
|
+
".cache",
|
|
1351
|
+
".parcel-cache",
|
|
1352
|
+
"target",
|
|
1353
|
+
".gradle",
|
|
1354
|
+
].includes(part));
|
|
1326
1355
|
}
|
|
1327
1356
|
function codeLanguage(path) {
|
|
1328
1357
|
const extension = extensionOf(path);
|
|
@@ -1384,14 +1413,184 @@ function codeFileKind(path) {
|
|
|
1384
1413
|
return "doc";
|
|
1385
1414
|
return "source";
|
|
1386
1415
|
}
|
|
1387
|
-
function
|
|
1388
|
-
return
|
|
1416
|
+
function emptyCodeIndexManifest(projectDir) {
|
|
1417
|
+
return {
|
|
1418
|
+
schema_version: 1,
|
|
1419
|
+
project_dir: projectDir,
|
|
1420
|
+
repo_key: repoKey(projectDir),
|
|
1421
|
+
generated_at: nowIso(),
|
|
1422
|
+
mode: "quick",
|
|
1423
|
+
limits: {
|
|
1424
|
+
max_file_bytes: MAX_CODE_FILE_BYTES,
|
|
1425
|
+
max_files: MAX_CODE_GRAPH_FILES,
|
|
1426
|
+
max_symbols: MAX_CODE_GRAPH_SYMBOLS,
|
|
1427
|
+
max_calls: MAX_CODE_GRAPH_CALLS,
|
|
1428
|
+
max_calls_per_file: MAX_CODE_GRAPH_CALLS_PER_FILE,
|
|
1429
|
+
},
|
|
1430
|
+
coverage: {
|
|
1431
|
+
indexable_files: 0,
|
|
1432
|
+
indexed_files: 0,
|
|
1433
|
+
deferred_files: 0,
|
|
1434
|
+
ignored_files: 0,
|
|
1435
|
+
coverage_percent: 100,
|
|
1436
|
+
complete: true,
|
|
1437
|
+
},
|
|
1438
|
+
cache: {
|
|
1439
|
+
hits: 0,
|
|
1440
|
+
misses: 0,
|
|
1441
|
+
},
|
|
1442
|
+
deferred_files: [],
|
|
1443
|
+
ignored_summary: {},
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function codeIndexManifestPath(projectDir) {
|
|
1447
|
+
return (0, node_path_1.join)(codeGraphDir(projectDir), "index-manifest.json");
|
|
1448
|
+
}
|
|
1449
|
+
function codeIndexSelection(projectDir) {
|
|
1450
|
+
const candidates = [];
|
|
1451
|
+
const deferred = [];
|
|
1452
|
+
const ignoredSummary = {};
|
|
1453
|
+
const ignore = (reason) => {
|
|
1454
|
+
ignoredSummary[reason] = (ignoredSummary[reason] ?? 0) + 1;
|
|
1455
|
+
};
|
|
1456
|
+
const visit = (dir) => {
|
|
1457
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
1458
|
+
return;
|
|
1459
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir)) {
|
|
1460
|
+
const absolutePath = (0, node_path_1.join)(dir, entry);
|
|
1461
|
+
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1462
|
+
if (shouldSkipCodePath(rel)) {
|
|
1463
|
+
ignore("generated_vendor_or_cache");
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
const stats = (0, node_fs_1.statSync)(absolutePath);
|
|
1467
|
+
if (stats.isDirectory()) {
|
|
1468
|
+
visit(absolutePath);
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
const extension = extensionOf(rel);
|
|
1472
|
+
const indexable = CODE_EXTENSIONS.has(extension) || CONFIG_NAMES.has((0, node_path_1.basename)(rel)) || rel === "README.md";
|
|
1473
|
+
if (!indexable) {
|
|
1474
|
+
ignore("unsupported_file_type");
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (stats.size > MAX_CODE_FILE_BYTES) {
|
|
1478
|
+
deferred.push({ path: rel, size_bytes: stats.size, reason: "over_quick_file_size_limit" });
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
candidates.push(absolutePath);
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
visit(projectDir);
|
|
1485
|
+
const sorted = candidates.sort((a, b) => codeFilePriority(projectDir, a) - codeFilePriority(projectDir, b) || a.localeCompare(b));
|
|
1486
|
+
const indexableFiles = sorted.length + deferred.length;
|
|
1487
|
+
const files = sorted.slice(0, MAX_CODE_GRAPH_FILES);
|
|
1488
|
+
for (const absolutePath of sorted.slice(MAX_CODE_GRAPH_FILES)) {
|
|
1389
1489
|
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1490
|
+
deferred.push({ path: rel, size_bytes: (0, node_fs_1.statSync)(absolutePath).size, reason: "over_quick_file_count_limit" });
|
|
1491
|
+
}
|
|
1492
|
+
const manifest = emptyCodeIndexManifest(projectDir);
|
|
1493
|
+
manifest.coverage = {
|
|
1494
|
+
indexable_files: indexableFiles,
|
|
1495
|
+
indexed_files: files.length,
|
|
1496
|
+
deferred_files: deferred.length,
|
|
1497
|
+
ignored_files: Object.values(ignoredSummary).reduce((sum, count) => sum + count, 0),
|
|
1498
|
+
coverage_percent: percent(files.length, indexableFiles),
|
|
1499
|
+
complete: deferred.length === 0,
|
|
1500
|
+
};
|
|
1501
|
+
manifest.deferred_files = deferred.sort((a, b) => a.path.localeCompare(b.path));
|
|
1502
|
+
manifest.ignored_summary = Object.fromEntries(Object.entries(ignoredSummary).sort(([a], [b]) => a.localeCompare(b)));
|
|
1503
|
+
return { files, manifest };
|
|
1504
|
+
}
|
|
1505
|
+
function writeCodeIndexManifest(projectDir, manifest) {
|
|
1506
|
+
writeJson(codeIndexManifestPath(projectDir), manifest);
|
|
1507
|
+
}
|
|
1508
|
+
function readCodeIndexManifest(projectDir) {
|
|
1509
|
+
const path = codeIndexManifestPath(projectDir);
|
|
1510
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
1511
|
+
return emptyCodeIndexManifest(projectDir);
|
|
1512
|
+
try {
|
|
1513
|
+
const manifest = readJson(path);
|
|
1514
|
+
if (!manifest.cache)
|
|
1515
|
+
manifest.cache = { hits: 0, misses: 0 };
|
|
1516
|
+
return manifest;
|
|
1517
|
+
}
|
|
1518
|
+
catch {
|
|
1519
|
+
return emptyCodeIndexManifest(projectDir);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function listCodeFiles(projectDir) {
|
|
1523
|
+
return codeIndexSelection(projectDir).files;
|
|
1524
|
+
}
|
|
1525
|
+
function fileFactCacheDir(projectDir) {
|
|
1526
|
+
return (0, node_path_1.join)(codeGraphDir(projectDir), "file-cache");
|
|
1527
|
+
}
|
|
1528
|
+
function fileFactCachePath(projectDir, rel, hash) {
|
|
1529
|
+
return (0, node_path_1.join)(fileFactCacheDir(projectDir), `${slugify(rel)}-${hash}.json`);
|
|
1530
|
+
}
|
|
1531
|
+
function readCachedFileFacts(projectDir, rel, hash) {
|
|
1532
|
+
const path = fileFactCachePath(projectDir, rel, hash);
|
|
1533
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
1534
|
+
return null;
|
|
1535
|
+
try {
|
|
1536
|
+
const cached = readJson(path);
|
|
1537
|
+
if (cached.schema_version !== 1 || cached.path !== rel || cached.hash !== hash)
|
|
1538
|
+
return null;
|
|
1539
|
+
if (!cached.file || !Array.isArray(cached.symbols) || !Array.isArray(cached.imports))
|
|
1540
|
+
return null;
|
|
1541
|
+
return cached;
|
|
1542
|
+
}
|
|
1543
|
+
catch {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function writeCachedFileFacts(projectDir, facts) {
|
|
1548
|
+
ensureDir(fileFactCacheDir(projectDir));
|
|
1549
|
+
writeJson(fileFactCachePath(projectDir, facts.path, facts.hash), facts);
|
|
1550
|
+
}
|
|
1551
|
+
function buildFileFacts(projectDir, absolutePath, knownFiles) {
|
|
1552
|
+
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1553
|
+
const content = (0, node_fs_1.readFileSync)(absolutePath, "utf8");
|
|
1554
|
+
const fullHash = (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
|
|
1555
|
+
const cached = readCachedFileFacts(projectDir, rel, fullHash);
|
|
1556
|
+
if (cached)
|
|
1557
|
+
return { facts: cached, content, cacheHit: true };
|
|
1558
|
+
const file = {
|
|
1559
|
+
id: `file:${slugify(rel)}`,
|
|
1560
|
+
path: rel,
|
|
1561
|
+
language: codeLanguage(rel),
|
|
1562
|
+
parser: codeParser(rel),
|
|
1563
|
+
kind: codeFileKind(rel),
|
|
1564
|
+
size_bytes: Buffer.byteLength(content),
|
|
1565
|
+
line_count: content.split(/\r?\n/).length,
|
|
1566
|
+
hash: fullHash.slice(0, 16),
|
|
1567
|
+
};
|
|
1568
|
+
const symbols = [];
|
|
1569
|
+
const imports = [];
|
|
1570
|
+
if (TS_AST_EXTENSIONS.has(extensionOf(rel))) {
|
|
1571
|
+
symbols.push(...extractSymbols(rel, content));
|
|
1572
|
+
imports.push(...extractImports(projectDir, rel, content, knownFiles));
|
|
1573
|
+
}
|
|
1574
|
+
else if (CODE_EXTENSIONS.has(extensionOf(rel))) {
|
|
1575
|
+
symbols.push(...extractGenericSymbols(rel, content));
|
|
1576
|
+
imports.push(...extractGenericImports(projectDir, rel, content, knownFiles));
|
|
1577
|
+
}
|
|
1578
|
+
const facts = { schema_version: 1, path: rel, hash: fullHash, file, symbols, imports };
|
|
1579
|
+
writeCachedFileFacts(projectDir, facts);
|
|
1580
|
+
return { facts, content, cacheHit: false };
|
|
1581
|
+
}
|
|
1582
|
+
function codeFilePriority(projectDir, absolutePath) {
|
|
1583
|
+
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1584
|
+
const kind = codeFileKind(rel);
|
|
1585
|
+
if (rel === "README.md" || CONFIG_NAMES.has((0, node_path_1.basename)(rel)))
|
|
1586
|
+
return 0;
|
|
1587
|
+
if (kind === "manifest" || kind === "config")
|
|
1588
|
+
return 1;
|
|
1589
|
+
if (kind === "test")
|
|
1590
|
+
return 2;
|
|
1591
|
+
if (TS_AST_EXTENSIONS.has(extensionOf(rel)))
|
|
1592
|
+
return 3;
|
|
1593
|
+
return 4;
|
|
1395
1594
|
}
|
|
1396
1595
|
function lineForOffset(text, offset) {
|
|
1397
1596
|
return text.slice(0, offset).split(/\r?\n/).length;
|
|
@@ -1779,6 +1978,8 @@ function extractCalls(path, text, symbols, symbolByName) {
|
|
|
1779
1978
|
const sourceFile = sourceFileFor(path, text);
|
|
1780
1979
|
const calls = [];
|
|
1781
1980
|
const visit = (node) => {
|
|
1981
|
+
if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
|
|
1982
|
+
return;
|
|
1782
1983
|
if (!ts.isCallExpression(node)) {
|
|
1783
1984
|
ts.forEachChild(node, visit);
|
|
1784
1985
|
return;
|
|
@@ -1796,6 +1997,8 @@ function extractCalls(path, text, symbols, symbolByName) {
|
|
|
1796
1997
|
const line = lineForNode(sourceFile, node);
|
|
1797
1998
|
const caller = symbolAtLine(symbols, path, line);
|
|
1798
1999
|
for (const target of targets.slice(0, 3)) {
|
|
2000
|
+
if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
|
|
2001
|
+
break;
|
|
1799
2002
|
if (target.path === path && target.line === line)
|
|
1800
2003
|
continue;
|
|
1801
2004
|
calls.push({ from_symbol: caller?.id ?? null, to_symbol: target.id, path, line });
|
|
@@ -1896,9 +2099,9 @@ function fileInputEntries(projectDir, paths, kind) {
|
|
|
1896
2099
|
sha256: sha256Hex((0, node_fs_1.readFileSync)(path)),
|
|
1897
2100
|
}));
|
|
1898
2101
|
}
|
|
1899
|
-
function codeGraphInputHash(projectDir) {
|
|
2102
|
+
function codeGraphInputHash(projectDir, absoluteFiles = listCodeFiles(projectDir)) {
|
|
1900
2103
|
return graphInputHash([
|
|
1901
|
-
...fileInputEntries(projectDir,
|
|
2104
|
+
...fileInputEntries(projectDir, absoluteFiles, "code_file"),
|
|
1902
2105
|
...fileInputEntries(projectDir, externalIndexFiles(projectDir).map((index) => index.path), "external_code_index"),
|
|
1903
2106
|
]);
|
|
1904
2107
|
}
|
|
@@ -2153,39 +2356,35 @@ function buildCodeGraph(projectDir) {
|
|
|
2153
2356
|
const head = gitHead(projectDir);
|
|
2154
2357
|
const tree = gitTree(projectDir);
|
|
2155
2358
|
const mergeBase = gitMergeBase(projectDir);
|
|
2156
|
-
const
|
|
2157
|
-
const absoluteFiles =
|
|
2359
|
+
const selection = codeIndexSelection(projectDir);
|
|
2360
|
+
const absoluteFiles = selection.files;
|
|
2361
|
+
const inputHash = codeGraphInputHash(projectDir, absoluteFiles);
|
|
2362
|
+
writeCodeIndexManifest(projectDir, selection.manifest);
|
|
2158
2363
|
const knownFiles = new Set(absoluteFiles.map((path) => (0, node_path_1.relative)(projectDir, path).replace(/\\/g, "/")));
|
|
2159
2364
|
const files = [];
|
|
2160
2365
|
const symbols = [];
|
|
2161
2366
|
const imports = [];
|
|
2162
2367
|
const contents = new Map();
|
|
2368
|
+
let cacheHits = 0;
|
|
2369
|
+
let cacheMisses = 0;
|
|
2163
2370
|
for (const absolutePath of absoluteFiles) {
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
});
|
|
2177
|
-
if (TS_AST_EXTENSIONS.has(extensionOf(rel))) {
|
|
2178
|
-
symbols.push(...extractSymbols(rel, content));
|
|
2179
|
-
imports.push(...extractImports(projectDir, rel, content, knownFiles));
|
|
2180
|
-
}
|
|
2181
|
-
else if (CODE_EXTENSIONS.has(extensionOf(rel))) {
|
|
2182
|
-
symbols.push(...extractGenericSymbols(rel, content));
|
|
2183
|
-
imports.push(...extractGenericImports(projectDir, rel, content, knownFiles));
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2371
|
+
const { facts, content, cacheHit } = buildFileFacts(projectDir, absolutePath, knownFiles);
|
|
2372
|
+
if (cacheHit)
|
|
2373
|
+
cacheHits++;
|
|
2374
|
+
else
|
|
2375
|
+
cacheMisses++;
|
|
2376
|
+
contents.set(facts.path, content);
|
|
2377
|
+
files.push(facts.file);
|
|
2378
|
+
symbols.push(...facts.symbols.slice(0, Math.max(0, MAX_CODE_GRAPH_SYMBOLS - symbols.length)));
|
|
2379
|
+
imports.push(...facts.imports);
|
|
2380
|
+
}
|
|
2381
|
+
selection.manifest.cache = { hits: cacheHits, misses: cacheMisses };
|
|
2382
|
+
writeCodeIndexManifest(projectDir, selection.manifest);
|
|
2186
2383
|
const externalFacts = loadExternalCodeFacts(projectDir);
|
|
2187
2384
|
const fileByPath = new Map(files.map((file) => [file.path, file]));
|
|
2188
2385
|
const addSymbol = (symbol) => {
|
|
2386
|
+
if (symbols.length >= MAX_CODE_GRAPH_SYMBOLS)
|
|
2387
|
+
return;
|
|
2189
2388
|
if (!fileByPath.has(symbol.path))
|
|
2190
2389
|
return;
|
|
2191
2390
|
const file = fileByPath.get(symbol.path);
|
|
@@ -2223,13 +2422,17 @@ function buildCodeGraph(projectDir) {
|
|
|
2223
2422
|
for (const [rel, content] of contents) {
|
|
2224
2423
|
if (!TS_AST_EXTENSIONS.has(extensionOf(rel)))
|
|
2225
2424
|
continue;
|
|
2425
|
+
if (calls.length >= MAX_CODE_GRAPH_CALLS)
|
|
2426
|
+
break;
|
|
2226
2427
|
const fileSymbols = symbols.filter((symbol) => symbol.path === rel);
|
|
2227
2428
|
const fileImports = imports.filter((item) => item.from_path === rel);
|
|
2228
|
-
calls.push(...extractCalls(rel, content,
|
|
2429
|
+
calls.push(...extractCalls(rel, content, fileSymbols, symbolByName).slice(0, Math.max(0, MAX_CODE_GRAPH_CALLS - calls.length)));
|
|
2229
2430
|
routes.push(...extractRoutes(rel, content, fileSymbols));
|
|
2230
2431
|
tests.push(...extractTests(rel, content, fileSymbols, fileImports));
|
|
2231
2432
|
}
|
|
2232
2433
|
for (const call of externalFacts.calls) {
|
|
2434
|
+
if (calls.length >= MAX_CODE_GRAPH_CALLS)
|
|
2435
|
+
break;
|
|
2233
2436
|
if (!calls.some((existing) => existing.from_symbol === call.from_symbol && existing.to_symbol === call.to_symbol && existing.path === call.path && existing.line === call.line))
|
|
2234
2437
|
calls.push(call);
|
|
2235
2438
|
}
|
|
@@ -2255,9 +2458,10 @@ function buildCodeGraph(projectDir) {
|
|
|
2255
2458
|
writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "tests.json"), graph.tests);
|
|
2256
2459
|
writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "packages.json"), graph.packages);
|
|
2257
2460
|
writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "graph.json"), graph);
|
|
2461
|
+
graphMemoryCache.delete((0, node_path_1.resolve)(projectDir));
|
|
2258
2462
|
return graph;
|
|
2259
2463
|
}
|
|
2260
|
-
function buildKnowledgeGraph(projectDir) {
|
|
2464
|
+
function buildKnowledgeGraph(projectDir, codeGraph = buildCodeGraph(projectDir)) {
|
|
2261
2465
|
ensureMemoryDirs(projectDir);
|
|
2262
2466
|
const packets = loadApprovedPackets(projectDir).sort((a, b) => a.id.localeCompare(b.id));
|
|
2263
2467
|
const branch = gitBranch(projectDir);
|
|
@@ -2269,7 +2473,6 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2269
2473
|
const episodes = [];
|
|
2270
2474
|
const repoEntityId = graphEntityId("repo", repoKey(projectDir));
|
|
2271
2475
|
const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
|
|
2272
|
-
const codeGraph = buildCodeGraph(projectDir);
|
|
2273
2476
|
const inputHash = knowledgeGraphInputHash(projectDir, codeGraph.repo_state.input_hash ?? codeGraphInputHash(projectDir));
|
|
2274
2477
|
addEntity(entities, {
|
|
2275
2478
|
id: repoEntityId,
|
|
@@ -2601,13 +2804,12 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2601
2804
|
writeJson((0, node_path_1.join)(graphDir(projectDir), "entities.json"), graph.entities);
|
|
2602
2805
|
writeJson((0, node_path_1.join)(graphDir(projectDir), "edges.json"), graph.edges);
|
|
2603
2806
|
writeJson((0, node_path_1.join)(graphDir(projectDir), "graph.json"), graph);
|
|
2807
|
+
graphMemoryCache.delete((0, node_path_1.resolve)(projectDir));
|
|
2604
2808
|
return graph;
|
|
2605
2809
|
}
|
|
2606
|
-
function
|
|
2810
|
+
function buildPacketIndexes(projectDir) {
|
|
2607
2811
|
ensureMemoryDirs(projectDir);
|
|
2608
2812
|
const packets = loadPacketsFromDir(packetsDir(projectDir)).sort((a, b) => a.id.localeCompare(b.id));
|
|
2609
|
-
const knowledgeGraph = buildKnowledgeGraph(projectDir);
|
|
2610
|
-
const codeGraph = buildCodeGraph(projectDir);
|
|
2611
2813
|
const byPath = {};
|
|
2612
2814
|
const byTag = {};
|
|
2613
2815
|
const byType = {};
|
|
@@ -2644,14 +2846,111 @@ function buildIndexes(projectDir) {
|
|
|
2644
2846
|
(0, node_path_1.join)(indexesDir(projectDir), "by-path.json"),
|
|
2645
2847
|
(0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
|
|
2646
2848
|
(0, node_path_1.join)(indexesDir(projectDir), "by-type.json"),
|
|
2647
|
-
(0, node_path_1.join)(indexesDir(projectDir), "graph.json"),
|
|
2648
|
-
(0, node_path_1.join)(indexesDir(projectDir), "code-graph.json"),
|
|
2649
2849
|
];
|
|
2650
2850
|
writeJson(written[0], catalog);
|
|
2651
2851
|
writeJson(written[1], byPath);
|
|
2652
2852
|
writeJson(written[2], byTag);
|
|
2653
2853
|
writeJson(written[3], byType);
|
|
2654
|
-
|
|
2854
|
+
return written;
|
|
2855
|
+
}
|
|
2856
|
+
function readCurrentCodeGraph(projectDir, expectedInputHash) {
|
|
2857
|
+
const path = (0, node_path_1.join)(codeGraphDir(projectDir), "graph.json");
|
|
2858
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
2859
|
+
return null;
|
|
2860
|
+
try {
|
|
2861
|
+
const graph = readJson(path);
|
|
2862
|
+
const inputHash = expectedInputHash ?? codeGraphInputHash(projectDir, codeIndexSelection(projectDir).files);
|
|
2863
|
+
if (graph.repo_state?.input_hash !== inputHash)
|
|
2864
|
+
return null;
|
|
2865
|
+
return graph;
|
|
2866
|
+
}
|
|
2867
|
+
catch {
|
|
2868
|
+
return null;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
function readCurrentKnowledgeGraph(projectDir, codeGraph, expectedInputHash) {
|
|
2872
|
+
const path = (0, node_path_1.join)(graphDir(projectDir), "graph.json");
|
|
2873
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
2874
|
+
return null;
|
|
2875
|
+
try {
|
|
2876
|
+
const graph = readJson(path);
|
|
2877
|
+
const inputHash = expectedInputHash ?? knowledgeGraphInputHash(projectDir, codeGraph.repo_state.input_hash ?? codeGraphInputHash(projectDir));
|
|
2878
|
+
if (graph.repo_state?.input_hash !== inputHash)
|
|
2879
|
+
return null;
|
|
2880
|
+
return graph;
|
|
2881
|
+
}
|
|
2882
|
+
catch {
|
|
2883
|
+
return null;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
function graphFastFingerprint(projectDir, selection = codeIndexSelection(projectDir)) {
|
|
2887
|
+
const packetPaths = (0, node_fs_1.existsSync)(packetsDir(projectDir))
|
|
2888
|
+
? (0, node_fs_1.readdirSync)(packetsDir(projectDir))
|
|
2889
|
+
.filter((name) => name.endsWith(".json"))
|
|
2890
|
+
.map((name) => (0, node_path_1.join)(packetsDir(projectDir), name))
|
|
2891
|
+
: [];
|
|
2892
|
+
const paths = [
|
|
2893
|
+
...selection.files,
|
|
2894
|
+
...externalIndexFiles(projectDir).map((index) => index.path),
|
|
2895
|
+
...packetPaths,
|
|
2896
|
+
];
|
|
2897
|
+
const entries = paths
|
|
2898
|
+
.filter((path) => (0, node_fs_1.existsSync)(path))
|
|
2899
|
+
.map((path) => {
|
|
2900
|
+
const stats = (0, node_fs_1.statSync)(path);
|
|
2901
|
+
return `${projectRelative(projectDir, path)}:${stats.size}:${Math.round(stats.mtimeMs)}`;
|
|
2902
|
+
})
|
|
2903
|
+
.sort();
|
|
2904
|
+
return sha256Hex(entries.join("\n"));
|
|
2905
|
+
}
|
|
2906
|
+
function readCurrentGraphs(projectDir) {
|
|
2907
|
+
const selection = codeIndexSelection(projectDir);
|
|
2908
|
+
const fingerprint = graphFastFingerprint(projectDir, selection);
|
|
2909
|
+
const cacheKey = (0, node_path_1.resolve)(projectDir);
|
|
2910
|
+
const cached = graphMemoryCache.get(cacheKey);
|
|
2911
|
+
if (cached?.fingerprint === fingerprint) {
|
|
2912
|
+
return { codeGraph: cached.codeGraph, knowledgeGraph: cached.knowledgeGraph };
|
|
2913
|
+
}
|
|
2914
|
+
const codeInputHash = codeGraphInputHash(projectDir, selection.files);
|
|
2915
|
+
const knowledgeInputHash = knowledgeGraphInputHash(projectDir, codeInputHash);
|
|
2916
|
+
if (cached?.codeInputHash === codeInputHash && cached.knowledgeInputHash === knowledgeInputHash) {
|
|
2917
|
+
cached.fingerprint = fingerprint;
|
|
2918
|
+
return { codeGraph: cached.codeGraph, knowledgeGraph: cached.knowledgeGraph };
|
|
2919
|
+
}
|
|
2920
|
+
const codeGraph = readCurrentCodeGraph(projectDir, codeInputHash);
|
|
2921
|
+
if (!codeGraph)
|
|
2922
|
+
return null;
|
|
2923
|
+
const knowledgeGraph = readCurrentKnowledgeGraph(projectDir, codeGraph, knowledgeInputHash);
|
|
2924
|
+
if (!knowledgeGraph)
|
|
2925
|
+
return null;
|
|
2926
|
+
graphMemoryCache.set(cacheKey, { fingerprint, codeInputHash, knowledgeInputHash, codeGraph, knowledgeGraph });
|
|
2927
|
+
return { codeGraph, knowledgeGraph };
|
|
2928
|
+
}
|
|
2929
|
+
function currentOrBuildGraphs(projectDir) {
|
|
2930
|
+
const current = readCurrentGraphs(projectDir);
|
|
2931
|
+
if (current?.codeGraph && current.knowledgeGraph) {
|
|
2932
|
+
return {
|
|
2933
|
+
indexes: [
|
|
2934
|
+
(0, node_path_1.join)(indexesDir(projectDir), "catalog.json"),
|
|
2935
|
+
(0, node_path_1.join)(indexesDir(projectDir), "by-path.json"),
|
|
2936
|
+
(0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
|
|
2937
|
+
(0, node_path_1.join)(indexesDir(projectDir), "by-type.json"),
|
|
2938
|
+
(0, node_path_1.join)(indexesDir(projectDir), "graph.json"),
|
|
2939
|
+
(0, node_path_1.join)(indexesDir(projectDir), "code-graph.json"),
|
|
2940
|
+
],
|
|
2941
|
+
codeGraph: current.codeGraph,
|
|
2942
|
+
knowledgeGraph: current.knowledgeGraph,
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
return buildGraphIndexes(projectDir);
|
|
2946
|
+
}
|
|
2947
|
+
function buildGraphIndexes(projectDir) {
|
|
2948
|
+
const written = buildPacketIndexes(projectDir);
|
|
2949
|
+
const codeGraph = buildCodeGraph(projectDir);
|
|
2950
|
+
const knowledgeGraph = buildKnowledgeGraph(projectDir, codeGraph);
|
|
2951
|
+
const graphIndexPath = (0, node_path_1.join)(indexesDir(projectDir), "graph.json");
|
|
2952
|
+
const codeGraphIndexPath = (0, node_path_1.join)(indexesDir(projectDir), "code-graph.json");
|
|
2953
|
+
writeJson(graphIndexPath, {
|
|
2655
2954
|
schema_version: knowledgeGraph.schema_version,
|
|
2656
2955
|
entities: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(graphDir(projectDir), "entities.json")),
|
|
2657
2956
|
edges: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(graphDir(projectDir), "edges.json")),
|
|
@@ -2660,7 +2959,7 @@ function buildIndexes(projectDir) {
|
|
|
2660
2959
|
edge_count: knowledgeGraph.edges.length,
|
|
2661
2960
|
episode_count: knowledgeGraph.episodes.length,
|
|
2662
2961
|
});
|
|
2663
|
-
writeJson(
|
|
2962
|
+
writeJson(codeGraphIndexPath, {
|
|
2664
2963
|
schema_version: codeGraph.schema_version,
|
|
2665
2964
|
files: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(codeGraphDir(projectDir), "files.json")),
|
|
2666
2965
|
symbols: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(codeGraphDir(projectDir), "symbols.json")),
|
|
@@ -2676,9 +2975,23 @@ function buildIndexes(projectDir) {
|
|
|
2676
2975
|
route_count: codeGraph.routes.length,
|
|
2677
2976
|
test_count: codeGraph.tests.length,
|
|
2678
2977
|
});
|
|
2679
|
-
|
|
2978
|
+
graphMemoryCache.set((0, node_path_1.resolve)(projectDir), {
|
|
2979
|
+
fingerprint: graphFastFingerprint(projectDir),
|
|
2980
|
+
codeInputHash: codeGraph.repo_state.input_hash ?? "",
|
|
2981
|
+
knowledgeInputHash: knowledgeGraph.repo_state.input_hash ?? "",
|
|
2982
|
+
codeGraph,
|
|
2983
|
+
knowledgeGraph,
|
|
2984
|
+
});
|
|
2985
|
+
return {
|
|
2986
|
+
indexes: [...written, graphIndexPath, codeGraphIndexPath],
|
|
2987
|
+
codeGraph,
|
|
2988
|
+
knowledgeGraph,
|
|
2989
|
+
};
|
|
2680
2990
|
}
|
|
2681
|
-
function
|
|
2991
|
+
function buildIndexes(projectDir) {
|
|
2992
|
+
return buildGraphIndexes(projectDir).indexes;
|
|
2993
|
+
}
|
|
2994
|
+
function indexProjectDetailed(projectDir, options = {}) {
|
|
2682
2995
|
ensureMemoryDirs(projectDir);
|
|
2683
2996
|
const policy = installAgentPolicy(projectDir);
|
|
2684
2997
|
const migrated = migrateLegacyMarkdown(projectDir);
|
|
@@ -2688,15 +3001,23 @@ function indexProject(projectDir) {
|
|
|
2688
3001
|
const structure = createRepoStructurePacket(projectDir);
|
|
2689
3002
|
if (structure)
|
|
2690
3003
|
upsertGeneratedPacket(projectDir, structure);
|
|
2691
|
-
const
|
|
3004
|
+
const built = options.graphs === false ? null : buildGraphIndexes(projectDir);
|
|
3005
|
+
const indexes = built?.indexes ?? buildPacketIndexes(projectDir);
|
|
2692
3006
|
return {
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
3007
|
+
result: {
|
|
3008
|
+
projectDir,
|
|
3009
|
+
packets: loadPacketsFromDir(packetsDir(projectDir)).length,
|
|
3010
|
+
migrated,
|
|
3011
|
+
indexes: indexes.map((path) => (0, node_path_1.relative)(projectDir, path)),
|
|
3012
|
+
policyPath: (0, node_path_1.relative)(projectDir, policy.path),
|
|
3013
|
+
},
|
|
3014
|
+
codeGraph: built?.codeGraph,
|
|
3015
|
+
knowledgeGraph: built?.knowledgeGraph,
|
|
2698
3016
|
};
|
|
2699
3017
|
}
|
|
3018
|
+
function indexProject(projectDir, options = {}) {
|
|
3019
|
+
return indexProjectDetailed(projectDir, options).result;
|
|
3020
|
+
}
|
|
2700
3021
|
function staleSuggestedAction(reasons) {
|
|
2701
3022
|
if (reasons.some((reason) => reason.includes("status is")))
|
|
2702
3023
|
return "mark_stale";
|
|
@@ -2755,11 +3076,20 @@ function refreshPacketStaleness(projectDir) {
|
|
|
2755
3076
|
return { findings, updated };
|
|
2756
3077
|
}
|
|
2757
3078
|
function refreshProject(projectDir) {
|
|
2758
|
-
const
|
|
3079
|
+
const detailedIndex = indexProjectDetailed(projectDir);
|
|
3080
|
+
const index = detailedIndex.result;
|
|
3081
|
+
let codeGraph = detailedIndex.codeGraph;
|
|
3082
|
+
let knowledgeGraph = detailedIndex.knowledgeGraph;
|
|
2759
3083
|
const stale = refreshPacketStaleness(projectDir);
|
|
2760
|
-
|
|
3084
|
+
let indexes = index.indexes;
|
|
3085
|
+
if (stale.updated > 0) {
|
|
3086
|
+
const rebuilt = buildGraphIndexes(projectDir);
|
|
3087
|
+
codeGraph = rebuilt.codeGraph;
|
|
3088
|
+
knowledgeGraph = rebuilt.knowledgeGraph;
|
|
3089
|
+
indexes = rebuilt.indexes.map((path) => (0, node_path_1.relative)(projectDir, path));
|
|
3090
|
+
}
|
|
2761
3091
|
const validation = validateProject(projectDir);
|
|
2762
|
-
const metrics =
|
|
3092
|
+
const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph, validation });
|
|
2763
3093
|
const nextActions = [];
|
|
2764
3094
|
if (stale.findings.length)
|
|
2765
3095
|
nextActions.push("Update, verify, or supersede stale repo memories before relying on them.");
|
|
@@ -2833,9 +3163,8 @@ function gcProject(projectDir, options = {}) {
|
|
|
2833
3163
|
}
|
|
2834
3164
|
}
|
|
2835
3165
|
if (!options.dryRun && (deprecated.length || deleted.length)) {
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
writeJson((0, node_path_1.join)(memoryRoot(projectDir), "metrics.json"), kageMetrics(projectDir));
|
|
3166
|
+
const rebuilt = buildGraphIndexes(projectDir);
|
|
3167
|
+
writeJson((0, node_path_1.join)(memoryRoot(projectDir), "metrics.json"), kageMetricsShallow(projectDir, rebuilt));
|
|
2839
3168
|
}
|
|
2840
3169
|
return {
|
|
2841
3170
|
ok: true,
|
|
@@ -3084,11 +3413,29 @@ function recallIntentBoost(queryTerms, packet) {
|
|
|
3084
3413
|
score += packet.type === "decision" ? 12 : 0;
|
|
3085
3414
|
return score;
|
|
3086
3415
|
}
|
|
3087
|
-
function
|
|
3088
|
-
const
|
|
3089
|
-
const
|
|
3416
|
+
function recallGraphLookup(graph) {
|
|
3417
|
+
const packetEntityByPacketId = new Map();
|
|
3418
|
+
for (const entity of graph.entities) {
|
|
3419
|
+
if (entity.type !== "memory")
|
|
3420
|
+
continue;
|
|
3421
|
+
for (const alias of entity.aliases)
|
|
3422
|
+
packetEntityByPacketId.set(alias, entity.id);
|
|
3423
|
+
}
|
|
3424
|
+
const edgesByEntityId = new Map();
|
|
3425
|
+
for (const edge of graph.edges) {
|
|
3426
|
+
const from = edgesByEntityId.get(edge.from) ?? [];
|
|
3427
|
+
from.push(edge);
|
|
3428
|
+
edgesByEntityId.set(edge.from, from);
|
|
3429
|
+
const to = edgesByEntityId.get(edge.to) ?? [];
|
|
3430
|
+
to.push(edge);
|
|
3431
|
+
edgesByEntityId.set(edge.to, to);
|
|
3432
|
+
}
|
|
3433
|
+
return { packetEntityByPacketId, edgesByEntityId };
|
|
3434
|
+
}
|
|
3435
|
+
function recallBreakdown(projectDir, terms, packet, textScore, graph = buildKnowledgeGraph(projectDir), lookup = recallGraphLookup(graph)) {
|
|
3436
|
+
const packetEntityId = lookup.packetEntityByPacketId.get(packet.id);
|
|
3090
3437
|
const rawGraphScore = packetEntityId
|
|
3091
|
-
?
|
|
3438
|
+
? (lookup.edgesByEntityId.get(packetEntityId) ?? []).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
|
|
3092
3439
|
: 0;
|
|
3093
3440
|
const graphScore = Math.min(rawGraphScore * 0.45, textScore > 0 ? textScore * 1.5 + 12 : 8);
|
|
3094
3441
|
const pathTypeTag = scoreText(terms, `${packet.type} ${packet.tags.join(" ")} ${packet.paths.join(" ")}`, [packet.type, ...packet.tags, ...packet.paths]);
|
|
@@ -3100,15 +3447,19 @@ function recallBreakdown(projectDir, terms, packet, textScore) {
|
|
|
3100
3447
|
const final = Number((textScore + graphScore + pathTypeTag * 0.8 + intent + vector + freshness + quality + feedback).toFixed(2));
|
|
3101
3448
|
return { bm25: textScore, text: textScore, graph: Number(graphScore.toFixed(2)), path_type_tag: pathTypeTag, intent, vector, freshness, quality: Number(quality.toFixed(2)), feedback, final };
|
|
3102
3449
|
}
|
|
3103
|
-
function recall(projectDir, query, limit = 5, explain = false) {
|
|
3104
|
-
|
|
3450
|
+
function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
|
|
3451
|
+
const current = inputs.codeGraph && inputs.knowledgeGraph ? null : readCurrentGraphs(projectDir);
|
|
3452
|
+
const detailedIndex = inputs.codeGraph && inputs.knowledgeGraph || current ? null : indexProjectDetailed(projectDir);
|
|
3453
|
+
const codeGraph = inputs.codeGraph ?? current?.codeGraph ?? detailedIndex?.codeGraph ?? buildCodeGraph(projectDir);
|
|
3454
|
+
const knowledgeGraph = inputs.knowledgeGraph ?? current?.knowledgeGraph ?? detailedIndex?.knowledgeGraph ?? buildKnowledgeGraph(projectDir, codeGraph);
|
|
3105
3455
|
const terms = tokenize(query);
|
|
3106
3456
|
const approvedPackets = loadApprovedPackets(projectDir);
|
|
3107
3457
|
const lexicalScores = scorePacketsBm25(terms, approvedPackets);
|
|
3458
|
+
const graphLookup = recallGraphLookup(knowledgeGraph);
|
|
3108
3459
|
const scored = approvedPackets
|
|
3109
3460
|
.map((packet) => {
|
|
3110
3461
|
const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
3111
|
-
const score_breakdown = recallBreakdown(projectDir, terms, packet, score);
|
|
3462
|
+
const score_breakdown = recallBreakdown(projectDir, terms, packet, score, knowledgeGraph, graphLookup);
|
|
3112
3463
|
const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
|
|
3113
3464
|
return { packet, score: score_breakdown.final, relevance, why_matched: why, score_breakdown };
|
|
3114
3465
|
})
|
|
@@ -3134,8 +3485,8 @@ function recall(projectDir, query, limit = 5, explain = false) {
|
|
|
3134
3485
|
return true;
|
|
3135
3486
|
})
|
|
3136
3487
|
.slice(0, 3);
|
|
3137
|
-
const graphContext = queryGraph(projectDir, query, 5);
|
|
3138
|
-
const codeContext = queryCodeGraph(projectDir, query, 5);
|
|
3488
|
+
const graphContext = queryGraph(projectDir, query, 5, knowledgeGraph);
|
|
3489
|
+
const codeContext = queryCodeGraph(projectDir, query, 5, codeGraph);
|
|
3139
3490
|
const lines = [
|
|
3140
3491
|
`# Kage Context`,
|
|
3141
3492
|
"",
|
|
@@ -3203,8 +3554,8 @@ function scoreText(terms, text, boosts = []) {
|
|
|
3203
3554
|
score += 3;
|
|
3204
3555
|
return score;
|
|
3205
3556
|
}
|
|
3206
|
-
function queryCodeGraph(projectDir, query, limit = 10) {
|
|
3207
|
-
|
|
3557
|
+
function queryCodeGraph(projectDir, query, limit = 10, graph) {
|
|
3558
|
+
graph = graph ?? readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
|
|
3208
3559
|
const terms = tokenize(query);
|
|
3209
3560
|
const files = graph.files
|
|
3210
3561
|
.map((file) => ({ file, score: scoreText(terms, `${file.path} ${file.kind} ${file.language} ${file.parser}`, [file.path, file.language]) }))
|
|
@@ -3268,8 +3619,8 @@ function queryCodeGraph(projectDir, query, limit = 10) {
|
|
|
3268
3619
|
];
|
|
3269
3620
|
return { query, context_block: lines.join("\n"), files, symbols, imports: imports.map((entry) => entry.item), calls, routes, tests };
|
|
3270
3621
|
}
|
|
3271
|
-
function queryGraph(projectDir, query, limit = 10) {
|
|
3272
|
-
|
|
3622
|
+
function queryGraph(projectDir, query, limit = 10, graph) {
|
|
3623
|
+
graph = graph ?? readCurrentGraphs(projectDir)?.knowledgeGraph ?? buildKnowledgeGraph(projectDir);
|
|
3273
3624
|
const terms = tokenize(query);
|
|
3274
3625
|
const entityScores = new Map();
|
|
3275
3626
|
for (const entity of graph.entities) {
|
|
@@ -3315,7 +3666,7 @@ function mermaidLabel(value) {
|
|
|
3315
3666
|
return value.replace(/["\n\r]/g, " ").slice(0, 80);
|
|
3316
3667
|
}
|
|
3317
3668
|
function graphMermaid(projectDir, limit = 40) {
|
|
3318
|
-
const graph = buildKnowledgeGraph(projectDir);
|
|
3669
|
+
const graph = readCurrentGraphs(projectDir)?.knowledgeGraph ?? buildKnowledgeGraph(projectDir);
|
|
3319
3670
|
const selectedEdges = graph.edges.slice(0, limit);
|
|
3320
3671
|
const selectedEntityIds = new Set(selectedEdges.flatMap((edge) => [edge.from, edge.to]));
|
|
3321
3672
|
const selectedEntities = graph.entities.filter((entity) => selectedEntityIds.has(entity.id));
|
|
@@ -3339,17 +3690,19 @@ function percent(numerator, denominator) {
|
|
|
3339
3690
|
}
|
|
3340
3691
|
function kageMetrics(projectDir) {
|
|
3341
3692
|
ensureMemoryDirs(projectDir);
|
|
3342
|
-
const
|
|
3343
|
-
const
|
|
3693
|
+
const built = currentOrBuildGraphs(projectDir);
|
|
3694
|
+
const codeGraph = built.codeGraph;
|
|
3695
|
+
const knowledgeGraph = built.knowledgeGraph;
|
|
3344
3696
|
const validation = validateProject(projectDir);
|
|
3345
3697
|
const approvedPackets = loadPacketsFromDir(packetsDir(projectDir)).length;
|
|
3346
3698
|
const pendingPackets = loadPacketsFromDir(pendingDir(projectDir)).length;
|
|
3347
3699
|
const evidenceBackedEdges = knowledgeGraph.edges.filter((edge) => edge.evidence.length > 0).length;
|
|
3348
3700
|
const policyPath = (0, node_path_1.join)(projectDir, "AGENTS.md");
|
|
3349
3701
|
const policyInstalled = (0, node_fs_1.existsSync)(policyPath) && (0, node_fs_1.readFileSync)(policyPath, "utf8").includes(AGENTS_POLICY_MARKER);
|
|
3702
|
+
const indexManifest = readCodeIndexManifest(projectDir);
|
|
3350
3703
|
const sourceFiles = codeGraph.files.filter((file) => file.kind === "source" || file.kind === "test");
|
|
3351
3704
|
const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
|
|
3352
|
-
const coverage = percent(indexedSourceFiles.length, sourceFiles.length);
|
|
3705
|
+
const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
|
|
3353
3706
|
const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
|
|
3354
3707
|
const qualityScores = allPackets
|
|
3355
3708
|
.map((packet) => Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score))
|
|
@@ -3371,7 +3724,7 @@ function kageMetrics(projectDir) {
|
|
|
3371
3724
|
(validation.ok ? 5 : -20) -
|
|
3372
3725
|
validation.warnings.length * 2)));
|
|
3373
3726
|
const quality = qualityReport(projectDir);
|
|
3374
|
-
const benchmark = benchmarkProject(projectDir);
|
|
3727
|
+
const benchmark = benchmarkProject(projectDir, { codeGraph, knowledgeGraph });
|
|
3375
3728
|
return {
|
|
3376
3729
|
schema_version: 1,
|
|
3377
3730
|
project_dir: projectDir,
|
|
@@ -3389,6 +3742,13 @@ function kageMetrics(projectDir) {
|
|
|
3389
3742
|
parsers: countBy(codeGraph.files, (file) => file.parser),
|
|
3390
3743
|
source_symbols_by_parser: countBy(codeGraph.symbols, (symbol) => symbol.parser),
|
|
3391
3744
|
indexer_coverage_percent: coverage,
|
|
3745
|
+
index_status: indexManifest.coverage.complete ? "complete" : "partial",
|
|
3746
|
+
indexable_files: indexManifest.coverage.indexable_files || sourceFiles.length,
|
|
3747
|
+
indexed_files: indexManifest.coverage.indexed_files || indexedSourceFiles.length,
|
|
3748
|
+
deferred_files: indexManifest.coverage.deferred_files,
|
|
3749
|
+
ignored_files: indexManifest.coverage.ignored_files,
|
|
3750
|
+
cache_hits: indexManifest.cache.hits,
|
|
3751
|
+
cache_misses: indexManifest.cache.misses,
|
|
3392
3752
|
},
|
|
3393
3753
|
memory_graph: {
|
|
3394
3754
|
approved_packets: approvedPackets,
|
|
@@ -3431,8 +3791,9 @@ function auditProject(projectDir) {
|
|
|
3431
3791
|
ensureMemoryDirs(projectDir);
|
|
3432
3792
|
const validation = validateProject(projectDir);
|
|
3433
3793
|
const quality = qualityReport(projectDir);
|
|
3434
|
-
const
|
|
3435
|
-
const
|
|
3794
|
+
const built = currentOrBuildGraphs(projectDir);
|
|
3795
|
+
const codeGraph = built.codeGraph;
|
|
3796
|
+
const knowledgeGraph = built.knowledgeGraph;
|
|
3436
3797
|
const approved = loadApprovedPackets(projectDir);
|
|
3437
3798
|
const pending = loadPendingPackets(projectDir);
|
|
3438
3799
|
const structuredPackets = approved.filter(hasStructuredEngineeringContext);
|
|
@@ -3681,8 +4042,11 @@ function qualityReport(projectDir) {
|
|
|
3681
4042
|
packets: rows,
|
|
3682
4043
|
};
|
|
3683
4044
|
}
|
|
3684
|
-
function benchmarkProject(projectDir) {
|
|
4045
|
+
function benchmarkProject(projectDir, inputs = {}) {
|
|
3685
4046
|
ensureMemoryDirs(projectDir);
|
|
4047
|
+
const built = inputs.codeGraph && inputs.knowledgeGraph ? null : currentOrBuildGraphs(projectDir);
|
|
4048
|
+
const codeGraph = inputs.codeGraph ?? built?.codeGraph;
|
|
4049
|
+
const knowledgeGraph = inputs.knowledgeGraph ?? built?.knowledgeGraph;
|
|
3686
4050
|
const scenarios = [
|
|
3687
4051
|
{ query: "how do I run tests", expected: "test" },
|
|
3688
4052
|
{ query: "where are routes defined", expected: "route" },
|
|
@@ -3690,7 +4054,7 @@ function benchmarkProject(projectDir) {
|
|
|
3690
4054
|
{ query: "what changed on this branch", expected: "branch" },
|
|
3691
4055
|
{ query: "what gotchas exist", expected: "gotcha" },
|
|
3692
4056
|
].map((scenario) => {
|
|
3693
|
-
const result = recall(projectDir, scenario.query, 5, true);
|
|
4057
|
+
const result = recall(projectDir, scenario.query, 5, true, { codeGraph, knowledgeGraph });
|
|
3694
4058
|
const text = `${result.context_block}\n${result.results.map((entry) => packetText(entry.packet)).join("\n")}`.toLowerCase();
|
|
3695
4059
|
return {
|
|
3696
4060
|
query: scenario.query,
|
|
@@ -3701,7 +4065,7 @@ function benchmarkProject(projectDir) {
|
|
|
3701
4065
|
context_tokens: estimateTokens(result.context_block),
|
|
3702
4066
|
};
|
|
3703
4067
|
});
|
|
3704
|
-
const metrics = kageMetricsShallow(projectDir);
|
|
4068
|
+
const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph });
|
|
3705
4069
|
const quality = qualityReport(projectDir);
|
|
3706
4070
|
const typeCoverage = quality.memory_type_coverage;
|
|
3707
4071
|
const recallHitRate = percent(scenarios.filter((scenario) => scenario.hit).length, scenarios.length);
|
|
@@ -3865,13 +4229,14 @@ function benchmarkTaskComparison(projectDir, task) {
|
|
|
3865
4229
|
],
|
|
3866
4230
|
};
|
|
3867
4231
|
}
|
|
3868
|
-
function kageMetricsShallow(projectDir) {
|
|
3869
|
-
const codeGraph = buildCodeGraph(projectDir);
|
|
3870
|
-
const knowledgeGraph = buildKnowledgeGraph(projectDir);
|
|
3871
|
-
const validation = validateProject(projectDir);
|
|
4232
|
+
function kageMetricsShallow(projectDir, inputs = {}) {
|
|
4233
|
+
const codeGraph = inputs.codeGraph ?? buildCodeGraph(projectDir);
|
|
4234
|
+
const knowledgeGraph = inputs.knowledgeGraph ?? buildKnowledgeGraph(projectDir, codeGraph);
|
|
4235
|
+
const validation = inputs.validation ?? validateProject(projectDir);
|
|
4236
|
+
const indexManifest = readCodeIndexManifest(projectDir);
|
|
3872
4237
|
const sourceFiles = codeGraph.files.filter((file) => file.kind === "source" || file.kind === "test");
|
|
3873
4238
|
const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
|
|
3874
|
-
const coverage = percent(indexedSourceFiles.length, sourceFiles.length);
|
|
4239
|
+
const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
|
|
3875
4240
|
const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
|
|
3876
4241
|
const indexedSourceTokens = Math.ceil(sourceFiles.reduce((sum, file) => sum + file.size_bytes, 0) / 4);
|
|
3877
4242
|
const memoryTokens = allPackets.reduce((sum, packet) => sum + estimateTokens(packetText(packet)), 0);
|
|
@@ -3893,6 +4258,13 @@ function kageMetricsShallow(projectDir) {
|
|
|
3893
4258
|
parsers: countBy(codeGraph.files, (file) => file.parser),
|
|
3894
4259
|
source_symbols_by_parser: countBy(codeGraph.symbols, (symbol) => symbol.parser),
|
|
3895
4260
|
indexer_coverage_percent: coverage,
|
|
4261
|
+
index_status: indexManifest.coverage.complete ? "complete" : "partial",
|
|
4262
|
+
indexable_files: indexManifest.coverage.indexable_files || sourceFiles.length,
|
|
4263
|
+
indexed_files: indexManifest.coverage.indexed_files || indexedSourceFiles.length,
|
|
4264
|
+
deferred_files: indexManifest.coverage.deferred_files,
|
|
4265
|
+
ignored_files: indexManifest.coverage.ignored_files,
|
|
4266
|
+
cache_hits: indexManifest.cache.hits,
|
|
4267
|
+
cache_misses: indexManifest.cache.misses,
|
|
3896
4268
|
},
|
|
3897
4269
|
memory_graph: {
|
|
3898
4270
|
approved_packets: loadPacketsFromDir(packetsDir(projectDir)).length,
|
|
@@ -5578,9 +5950,9 @@ function installClaudeSettings(projectDir) {
|
|
|
5578
5950
|
function initProject(projectDir) {
|
|
5579
5951
|
installAgentPolicy(projectDir);
|
|
5580
5952
|
installClaudeSettings(projectDir);
|
|
5581
|
-
const index = indexProject(projectDir);
|
|
5953
|
+
const index = indexProject(projectDir, { graphs: false });
|
|
5582
5954
|
const validation = validateProject(projectDir);
|
|
5583
|
-
const sampleRecall =
|
|
5955
|
+
const sampleRecall = recallFromPackets("how do I run tests", loadApprovedPackets(projectDir), 5, "Repo Memory");
|
|
5584
5956
|
return { index, validation, sampleRecall };
|
|
5585
5957
|
}
|
|
5586
5958
|
function doctorProject(projectDir) {
|
package/package.json
CHANGED
package/viewer/app.js
CHANGED
|
@@ -112,6 +112,7 @@
|
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
var MEMORY_CODE_RELATIONS = new Set(["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test", "affects_path"]);
|
|
115
|
+
var INSPECTOR_CONNECTION_LIMIT = 8;
|
|
115
116
|
|
|
116
117
|
els.graphFile.addEventListener("change", handleFile);
|
|
117
118
|
els.searchInput.addEventListener("input", scheduleRender);
|
|
@@ -1373,6 +1374,97 @@
|
|
|
1373
1374
|
els.selectionDetails.appendChild(title);
|
|
1374
1375
|
els.selectionDetails.appendChild(kind);
|
|
1375
1376
|
els.selectionDetails.appendChild(rows);
|
|
1377
|
+
renderInspectorConnections(item);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function renderInspectorConnections(item) {
|
|
1381
|
+
if (state.selected.kind !== "entity") {
|
|
1382
|
+
if (item && isMemoryCodeEdge(item)) {
|
|
1383
|
+
els.selectionDetails.appendChild(detailSection("Memory-Code Link", [
|
|
1384
|
+
connectionText(item, state.entityById.get(item.from), state.entityById.get(item.to))
|
|
1385
|
+
], 0));
|
|
1386
|
+
}
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
var links = memoryCodeConnections(item.id);
|
|
1391
|
+
if (!links.length) return;
|
|
1392
|
+
var rows = links.slice(0, INSPECTOR_CONNECTION_LIMIT).map(function (link) {
|
|
1393
|
+
return connectionText(link.edge, item, link.other);
|
|
1394
|
+
});
|
|
1395
|
+
els.selectionDetails.appendChild(detailSection("Memory-Code Evidence", rows, links.length - rows.length));
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function memoryCodeConnections(entityId) {
|
|
1399
|
+
return state.edges
|
|
1400
|
+
.filter(function (edge) { return isMemoryCodeEdge(edge) && (edge.from === entityId || edge.to === entityId); })
|
|
1401
|
+
.map(function (edge) {
|
|
1402
|
+
var otherId = edge.from === entityId ? edge.to : edge.from;
|
|
1403
|
+
return { edge: edge, other: state.entityById.get(otherId) };
|
|
1404
|
+
})
|
|
1405
|
+
.filter(function (link) { return Boolean(link.other); })
|
|
1406
|
+
.sort(function (a, b) {
|
|
1407
|
+
return connectionImportance(b) - connectionImportance(a) ||
|
|
1408
|
+
displayName(a.other).localeCompare(displayName(b.other));
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function isMemoryCodeEdge(edge) {
|
|
1413
|
+
return Boolean(edge && (edge.memory_code_link || isMemoryCodeRelation(edge.relation)));
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function connectionImportance(link) {
|
|
1417
|
+
var relation = String(link.edge.relation || "");
|
|
1418
|
+
var score = entityImportance(link.other);
|
|
1419
|
+
if (["fixes_symbol", "verified_by_test"].indexOf(relation) !== -1) score += 36;
|
|
1420
|
+
if (["explains_symbol", "applies_to_route"].indexOf(relation) !== -1) score += 24;
|
|
1421
|
+
if (link.other.graph_kind === "memory") score += 18;
|
|
1422
|
+
return score;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function connectionText(edge, selected, other) {
|
|
1426
|
+
var from = state.entityById.get(edge.from);
|
|
1427
|
+
var to = state.entityById.get(edge.to);
|
|
1428
|
+
var peer = other || (selected && selected.id === edge.from ? to : from);
|
|
1429
|
+
var label = peer ? displayName(peer) : displayName(from) + " -> " + displayName(to);
|
|
1430
|
+
var meta = [edge.relation || "related", peer && (peer.graph_kind || peer.type)].filter(Boolean).join(" | ");
|
|
1431
|
+
return {
|
|
1432
|
+
label: label,
|
|
1433
|
+
meta: meta,
|
|
1434
|
+
body: edge.fact || "",
|
|
1435
|
+
edge: edge,
|
|
1436
|
+
entity: peer
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function detailSection(title, items, hiddenCount) {
|
|
1441
|
+
var section = document.createElement("section");
|
|
1442
|
+
section.className = "detail-section";
|
|
1443
|
+
var heading = document.createElement("div");
|
|
1444
|
+
heading.className = "detail-section-title";
|
|
1445
|
+
heading.textContent = title;
|
|
1446
|
+
section.appendChild(heading);
|
|
1447
|
+
items.forEach(function (item) {
|
|
1448
|
+
var button = document.createElement("button");
|
|
1449
|
+
button.type = "button";
|
|
1450
|
+
button.className = "detail-link";
|
|
1451
|
+
button.innerHTML = "<span class=\"detail-link-title\"></span><span class=\"detail-link-meta\"></span><span class=\"detail-link-body\"></span>";
|
|
1452
|
+
button.querySelector(".detail-link-title").textContent = item.label;
|
|
1453
|
+
button.querySelector(".detail-link-meta").textContent = item.meta;
|
|
1454
|
+
button.querySelector(".detail-link-body").textContent = item.body;
|
|
1455
|
+
button.addEventListener("click", function () {
|
|
1456
|
+
state.selected = item.entity ? { kind: "entity", id: item.entity.id } : { kind: "edge", id: item.edge.id };
|
|
1457
|
+
render();
|
|
1458
|
+
});
|
|
1459
|
+
section.appendChild(button);
|
|
1460
|
+
});
|
|
1461
|
+
if (hiddenCount > 0) {
|
|
1462
|
+
var more = document.createElement("div");
|
|
1463
|
+
more.className = "detail-more";
|
|
1464
|
+
more.textContent = "+" + hiddenCount + " more connected items hidden to keep the graph readable.";
|
|
1465
|
+
section.appendChild(more);
|
|
1466
|
+
}
|
|
1467
|
+
return section;
|
|
1376
1468
|
}
|
|
1377
1469
|
|
|
1378
1470
|
function detailRow(label, value) {
|
|
@@ -1727,7 +1819,6 @@
|
|
|
1727
1819
|
var node = findCanvasNode(world.x, world.y);
|
|
1728
1820
|
if (!node) return;
|
|
1729
1821
|
state.selected = { kind: "entity", id: node.id };
|
|
1730
|
-
els.scopeFilter.value = "focus";
|
|
1731
1822
|
render();
|
|
1732
1823
|
}
|
|
1733
1824
|
|
package/viewer/index.html
CHANGED
|
@@ -86,21 +86,14 @@
|
|
|
86
86
|
<option value="">All relations</option>
|
|
87
87
|
</select>
|
|
88
88
|
</label>
|
|
89
|
-
<
|
|
90
|
-
<span>Scope</span>
|
|
91
|
-
<select id="scopeFilter">
|
|
92
|
-
<option value="signal">High signal</option>
|
|
93
|
-
<option value="focus">Focus selection</option>
|
|
94
|
-
<option value="all">Everything</option>
|
|
95
|
-
</select>
|
|
96
|
-
</label>
|
|
89
|
+
<input id="scopeFilter" type="hidden" value="signal">
|
|
97
90
|
<label>
|
|
98
91
|
<span>Max Nodes</span>
|
|
99
92
|
<select id="maxNodes">
|
|
100
93
|
<option value="60">60</option>
|
|
101
94
|
<option value="90" selected>90</option>
|
|
102
95
|
<option value="140">140</option>
|
|
103
|
-
<option value="
|
|
96
|
+
<option value="220">220</option>
|
|
104
97
|
</select>
|
|
105
98
|
</label>
|
|
106
99
|
<label class="toggle-control">
|
package/viewer/styles.css
CHANGED
|
@@ -435,6 +435,55 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
435
435
|
.detail-row { padding: 9px 0; border-top: 1px solid var(--line); }
|
|
436
436
|
.detail-row dt { margin: 0 0 4px; color: var(--terminal-dim); font-size: 11px; font-weight: 760; text-transform: uppercase; }
|
|
437
437
|
.detail-row dd { margin: 0; color: var(--text); overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
438
|
+
.detail-section {
|
|
439
|
+
margin-top: 12px;
|
|
440
|
+
padding-top: 12px;
|
|
441
|
+
border-top: 1px solid var(--line);
|
|
442
|
+
}
|
|
443
|
+
.detail-section-title {
|
|
444
|
+
margin-bottom: 8px;
|
|
445
|
+
color: var(--terminal-dim);
|
|
446
|
+
font-size: 11px;
|
|
447
|
+
font-weight: 800;
|
|
448
|
+
text-transform: uppercase;
|
|
449
|
+
}
|
|
450
|
+
.detail-link {
|
|
451
|
+
width: 100%;
|
|
452
|
+
display: grid;
|
|
453
|
+
gap: 4px;
|
|
454
|
+
min-height: 0;
|
|
455
|
+
margin-top: 8px;
|
|
456
|
+
padding: 9px;
|
|
457
|
+
border-color: rgba(216, 255, 95, 0.28);
|
|
458
|
+
background: rgba(216, 255, 95, 0.045);
|
|
459
|
+
color: var(--text);
|
|
460
|
+
text-align: left;
|
|
461
|
+
white-space: normal;
|
|
462
|
+
box-shadow: none;
|
|
463
|
+
}
|
|
464
|
+
.detail-link:hover { background: rgba(216, 255, 95, 0.085); }
|
|
465
|
+
.detail-link-title {
|
|
466
|
+
color: var(--terminal);
|
|
467
|
+
font-weight: 820;
|
|
468
|
+
overflow-wrap: anywhere;
|
|
469
|
+
}
|
|
470
|
+
.detail-link-meta {
|
|
471
|
+
color: var(--warn);
|
|
472
|
+
font-size: 10px;
|
|
473
|
+
font-weight: 760;
|
|
474
|
+
text-transform: uppercase;
|
|
475
|
+
overflow-wrap: anywhere;
|
|
476
|
+
}
|
|
477
|
+
.detail-link-body {
|
|
478
|
+
color: var(--muted);
|
|
479
|
+
font-size: 11px;
|
|
480
|
+
overflow-wrap: anywhere;
|
|
481
|
+
}
|
|
482
|
+
.detail-more {
|
|
483
|
+
margin-top: 9px;
|
|
484
|
+
color: var(--terminal-dim);
|
|
485
|
+
font-size: 11px;
|
|
486
|
+
}
|
|
438
487
|
|
|
439
488
|
.entities-panel { grid-area: entities; }
|
|
440
489
|
.edges-panel { grid-area: edges; }
|