@kage-core/kage-graph-mcp 1.1.17 → 1.1.19

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 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) => [".git", ".agent_memory", "node_modules", "dist", "build", "coverage", ".next", ".turbo"].includes(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,214 @@ function codeFileKind(path) {
1384
1413
  return "doc";
1385
1414
  return "source";
1386
1415
  }
1387
- function listCodeFiles(projectDir) {
1388
- return walkFiles(projectDir, (absolutePath) => {
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
- if (shouldSkipCodePath(rel))
1391
- return false;
1392
- const extension = extensionOf(rel);
1393
- return CODE_EXTENSIONS.has(extension) || CONFIG_NAMES.has((0, node_path_1.basename)(rel)) || rel === "README.md";
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 codeGraphStatFingerprint(projectDir, absoluteFiles) {
1526
+ const entries = [
1527
+ ...absoluteFiles,
1528
+ ...externalIndexFiles(projectDir).map((index) => index.path),
1529
+ ...["package.json", "requirements.txt", "go.mod", "Cargo.toml"]
1530
+ .map((path) => (0, node_path_1.join)(projectDir, path))
1531
+ .filter((path) => (0, node_fs_1.existsSync)(path)),
1532
+ ]
1533
+ .filter((path) => (0, node_fs_1.existsSync)(path))
1534
+ .map((path) => {
1535
+ const stats = (0, node_fs_1.statSync)(path);
1536
+ return `${projectRelative(projectDir, path)}:${stats.size}:${Math.round(stats.mtimeMs)}`;
1537
+ })
1538
+ .sort();
1539
+ return sha256Hex(entries.join("\n"));
1540
+ }
1541
+ function readCachedCodeGraph(projectDir, fingerprint) {
1542
+ const path = (0, node_path_1.join)(codeGraphDir(projectDir), "graph.json");
1543
+ if (!(0, node_fs_1.existsSync)(path))
1544
+ return null;
1545
+ try {
1546
+ const graph = readJson(path);
1547
+ if (readCodeIndexManifest(projectDir).fingerprint !== fingerprint)
1548
+ return null;
1549
+ return graph;
1550
+ }
1551
+ catch {
1552
+ return null;
1553
+ }
1554
+ }
1555
+ function fileFactCacheDir(projectDir) {
1556
+ return (0, node_path_1.join)(codeGraphDir(projectDir), "file-cache");
1557
+ }
1558
+ function fileFactCachePath(projectDir, rel, hash) {
1559
+ return (0, node_path_1.join)(fileFactCacheDir(projectDir), `${slugify(rel)}-${hash}.json`);
1560
+ }
1561
+ function readCachedFileFacts(projectDir, rel, hash) {
1562
+ const path = fileFactCachePath(projectDir, rel, hash);
1563
+ if (!(0, node_fs_1.existsSync)(path))
1564
+ return null;
1565
+ try {
1566
+ const cached = readJson(path);
1567
+ if (cached.schema_version !== 1 || cached.path !== rel || cached.hash !== hash)
1568
+ return null;
1569
+ if (!cached.file || !Array.isArray(cached.symbols) || !Array.isArray(cached.imports))
1570
+ return null;
1571
+ return cached;
1572
+ }
1573
+ catch {
1574
+ return null;
1575
+ }
1576
+ }
1577
+ function writeCachedFileFacts(projectDir, facts) {
1578
+ ensureDir(fileFactCacheDir(projectDir));
1579
+ writeJson(fileFactCachePath(projectDir, facts.path, facts.hash), facts);
1580
+ }
1581
+ function buildFileFacts(projectDir, absolutePath, knownFiles) {
1582
+ const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
1583
+ const content = (0, node_fs_1.readFileSync)(absolutePath, "utf8");
1584
+ const fullHash = (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
1585
+ const cached = readCachedFileFacts(projectDir, rel, fullHash);
1586
+ if (cached)
1587
+ return { facts: cached, content, cacheHit: true };
1588
+ const file = {
1589
+ id: `file:${slugify(rel)}`,
1590
+ path: rel,
1591
+ language: codeLanguage(rel),
1592
+ parser: codeParser(rel),
1593
+ kind: codeFileKind(rel),
1594
+ size_bytes: Buffer.byteLength(content),
1595
+ line_count: content.split(/\r?\n/).length,
1596
+ hash: fullHash.slice(0, 16),
1597
+ };
1598
+ const symbols = [];
1599
+ const imports = [];
1600
+ if (TS_AST_EXTENSIONS.has(extensionOf(rel))) {
1601
+ symbols.push(...extractSymbols(rel, content));
1602
+ imports.push(...extractImports(projectDir, rel, content, knownFiles));
1603
+ }
1604
+ else if (CODE_EXTENSIONS.has(extensionOf(rel))) {
1605
+ symbols.push(...extractGenericSymbols(rel, content));
1606
+ imports.push(...extractGenericImports(projectDir, rel, content, knownFiles));
1607
+ }
1608
+ const facts = { schema_version: 1, path: rel, hash: fullHash, file, symbols, imports };
1609
+ writeCachedFileFacts(projectDir, facts);
1610
+ return { facts, content, cacheHit: false };
1611
+ }
1612
+ function codeFilePriority(projectDir, absolutePath) {
1613
+ const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
1614
+ const kind = codeFileKind(rel);
1615
+ if (rel === "README.md" || CONFIG_NAMES.has((0, node_path_1.basename)(rel)))
1616
+ return 0;
1617
+ if (kind === "manifest" || kind === "config")
1618
+ return 1;
1619
+ if (kind === "test")
1620
+ return 2;
1621
+ if (TS_AST_EXTENSIONS.has(extensionOf(rel)))
1622
+ return 3;
1623
+ return 4;
1395
1624
  }
1396
1625
  function lineForOffset(text, offset) {
1397
1626
  return text.slice(0, offset).split(/\r?\n/).length;
@@ -1779,6 +2008,8 @@ function extractCalls(path, text, symbols, symbolByName) {
1779
2008
  const sourceFile = sourceFileFor(path, text);
1780
2009
  const calls = [];
1781
2010
  const visit = (node) => {
2011
+ if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
2012
+ return;
1782
2013
  if (!ts.isCallExpression(node)) {
1783
2014
  ts.forEachChild(node, visit);
1784
2015
  return;
@@ -1796,6 +2027,8 @@ function extractCalls(path, text, symbols, symbolByName) {
1796
2027
  const line = lineForNode(sourceFile, node);
1797
2028
  const caller = symbolAtLine(symbols, path, line);
1798
2029
  for (const target of targets.slice(0, 3)) {
2030
+ if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
2031
+ break;
1799
2032
  if (target.path === path && target.line === line)
1800
2033
  continue;
1801
2034
  calls.push({ from_symbol: caller?.id ?? null, to_symbol: target.id, path, line });
@@ -1896,9 +2129,9 @@ function fileInputEntries(projectDir, paths, kind) {
1896
2129
  sha256: sha256Hex((0, node_fs_1.readFileSync)(path)),
1897
2130
  }));
1898
2131
  }
1899
- function codeGraphInputHash(projectDir) {
2132
+ function codeGraphInputHash(projectDir, absoluteFiles = listCodeFiles(projectDir)) {
1900
2133
  return graphInputHash([
1901
- ...fileInputEntries(projectDir, listCodeFiles(projectDir), "code_file"),
2134
+ ...fileInputEntries(projectDir, absoluteFiles, "code_file"),
1902
2135
  ...fileInputEntries(projectDir, externalIndexFiles(projectDir).map((index) => index.path), "external_code_index"),
1903
2136
  ]);
1904
2137
  }
@@ -2153,39 +2386,44 @@ function buildCodeGraph(projectDir) {
2153
2386
  const head = gitHead(projectDir);
2154
2387
  const tree = gitTree(projectDir);
2155
2388
  const mergeBase = gitMergeBase(projectDir);
2156
- const inputHash = codeGraphInputHash(projectDir);
2157
- const absoluteFiles = listCodeFiles(projectDir);
2389
+ const selection = codeIndexSelection(projectDir);
2390
+ const absoluteFiles = selection.files;
2391
+ const fingerprint = codeGraphStatFingerprint(projectDir, absoluteFiles);
2392
+ const cachedGraph = readCachedCodeGraph(projectDir, fingerprint);
2393
+ if (cachedGraph) {
2394
+ selection.manifest.cache = { hits: absoluteFiles.length, misses: 0 };
2395
+ selection.manifest.fingerprint = fingerprint;
2396
+ writeCodeIndexManifest(projectDir, selection.manifest);
2397
+ return cachedGraph;
2398
+ }
2399
+ const inputHash = codeGraphInputHash(projectDir, absoluteFiles);
2400
+ selection.manifest.fingerprint = fingerprint;
2401
+ writeCodeIndexManifest(projectDir, selection.manifest);
2158
2402
  const knownFiles = new Set(absoluteFiles.map((path) => (0, node_path_1.relative)(projectDir, path).replace(/\\/g, "/")));
2159
2403
  const files = [];
2160
2404
  const symbols = [];
2161
2405
  const imports = [];
2162
2406
  const contents = new Map();
2407
+ let cacheHits = 0;
2408
+ let cacheMisses = 0;
2163
2409
  for (const absolutePath of absoluteFiles) {
2164
- const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
2165
- const content = (0, node_fs_1.readFileSync)(absolutePath, "utf8");
2166
- contents.set(rel, content);
2167
- files.push({
2168
- id: `file:${slugify(rel)}`,
2169
- path: rel,
2170
- language: codeLanguage(rel),
2171
- parser: codeParser(rel),
2172
- kind: codeFileKind(rel),
2173
- size_bytes: Buffer.byteLength(content),
2174
- line_count: content.split(/\r?\n/).length,
2175
- hash: (0, node_crypto_1.createHash)("sha256").update(content).digest("hex").slice(0, 16),
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
- }
2410
+ const { facts, content, cacheHit } = buildFileFacts(projectDir, absolutePath, knownFiles);
2411
+ if (cacheHit)
2412
+ cacheHits++;
2413
+ else
2414
+ cacheMisses++;
2415
+ contents.set(facts.path, content);
2416
+ files.push(facts.file);
2417
+ symbols.push(...facts.symbols.slice(0, Math.max(0, MAX_CODE_GRAPH_SYMBOLS - symbols.length)));
2418
+ imports.push(...facts.imports);
2419
+ }
2420
+ selection.manifest.cache = { hits: cacheHits, misses: cacheMisses };
2421
+ writeCodeIndexManifest(projectDir, selection.manifest);
2186
2422
  const externalFacts = loadExternalCodeFacts(projectDir);
2187
2423
  const fileByPath = new Map(files.map((file) => [file.path, file]));
2188
2424
  const addSymbol = (symbol) => {
2425
+ if (symbols.length >= MAX_CODE_GRAPH_SYMBOLS)
2426
+ return;
2189
2427
  if (!fileByPath.has(symbol.path))
2190
2428
  return;
2191
2429
  const file = fileByPath.get(symbol.path);
@@ -2223,13 +2461,17 @@ function buildCodeGraph(projectDir) {
2223
2461
  for (const [rel, content] of contents) {
2224
2462
  if (!TS_AST_EXTENSIONS.has(extensionOf(rel)))
2225
2463
  continue;
2464
+ if (calls.length >= MAX_CODE_GRAPH_CALLS)
2465
+ break;
2226
2466
  const fileSymbols = symbols.filter((symbol) => symbol.path === rel);
2227
2467
  const fileImports = imports.filter((item) => item.from_path === rel);
2228
- calls.push(...extractCalls(rel, content, symbols, symbolByName));
2468
+ calls.push(...extractCalls(rel, content, fileSymbols, symbolByName).slice(0, Math.max(0, MAX_CODE_GRAPH_CALLS - calls.length)));
2229
2469
  routes.push(...extractRoutes(rel, content, fileSymbols));
2230
2470
  tests.push(...extractTests(rel, content, fileSymbols, fileImports));
2231
2471
  }
2232
2472
  for (const call of externalFacts.calls) {
2473
+ if (calls.length >= MAX_CODE_GRAPH_CALLS)
2474
+ break;
2233
2475
  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
2476
  calls.push(call);
2235
2477
  }
@@ -2255,9 +2497,10 @@ function buildCodeGraph(projectDir) {
2255
2497
  writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "tests.json"), graph.tests);
2256
2498
  writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "packages.json"), graph.packages);
2257
2499
  writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "graph.json"), graph);
2500
+ graphMemoryCache.delete((0, node_path_1.resolve)(projectDir));
2258
2501
  return graph;
2259
2502
  }
2260
- function buildKnowledgeGraph(projectDir) {
2503
+ function buildKnowledgeGraph(projectDir, codeGraph = buildCodeGraph(projectDir)) {
2261
2504
  ensureMemoryDirs(projectDir);
2262
2505
  const packets = loadApprovedPackets(projectDir).sort((a, b) => a.id.localeCompare(b.id));
2263
2506
  const branch = gitBranch(projectDir);
@@ -2269,7 +2512,6 @@ function buildKnowledgeGraph(projectDir) {
2269
2512
  const episodes = [];
2270
2513
  const repoEntityId = graphEntityId("repo", repoKey(projectDir));
2271
2514
  const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
2272
- const codeGraph = buildCodeGraph(projectDir);
2273
2515
  const inputHash = knowledgeGraphInputHash(projectDir, codeGraph.repo_state.input_hash ?? codeGraphInputHash(projectDir));
2274
2516
  addEntity(entities, {
2275
2517
  id: repoEntityId,
@@ -2601,13 +2843,12 @@ function buildKnowledgeGraph(projectDir) {
2601
2843
  writeJson((0, node_path_1.join)(graphDir(projectDir), "entities.json"), graph.entities);
2602
2844
  writeJson((0, node_path_1.join)(graphDir(projectDir), "edges.json"), graph.edges);
2603
2845
  writeJson((0, node_path_1.join)(graphDir(projectDir), "graph.json"), graph);
2846
+ graphMemoryCache.delete((0, node_path_1.resolve)(projectDir));
2604
2847
  return graph;
2605
2848
  }
2606
- function buildIndexes(projectDir) {
2849
+ function buildPacketIndexes(projectDir) {
2607
2850
  ensureMemoryDirs(projectDir);
2608
2851
  const packets = loadPacketsFromDir(packetsDir(projectDir)).sort((a, b) => a.id.localeCompare(b.id));
2609
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
2610
- const codeGraph = buildCodeGraph(projectDir);
2611
2852
  const byPath = {};
2612
2853
  const byTag = {};
2613
2854
  const byType = {};
@@ -2644,14 +2885,111 @@ function buildIndexes(projectDir) {
2644
2885
  (0, node_path_1.join)(indexesDir(projectDir), "by-path.json"),
2645
2886
  (0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
2646
2887
  (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
2888
  ];
2650
2889
  writeJson(written[0], catalog);
2651
2890
  writeJson(written[1], byPath);
2652
2891
  writeJson(written[2], byTag);
2653
2892
  writeJson(written[3], byType);
2654
- writeJson(written[4], {
2893
+ return written;
2894
+ }
2895
+ function readCurrentCodeGraph(projectDir, expectedInputHash) {
2896
+ const path = (0, node_path_1.join)(codeGraphDir(projectDir), "graph.json");
2897
+ if (!(0, node_fs_1.existsSync)(path))
2898
+ return null;
2899
+ try {
2900
+ const graph = readJson(path);
2901
+ const inputHash = expectedInputHash ?? codeGraphInputHash(projectDir, codeIndexSelection(projectDir).files);
2902
+ if (graph.repo_state?.input_hash !== inputHash)
2903
+ return null;
2904
+ return graph;
2905
+ }
2906
+ catch {
2907
+ return null;
2908
+ }
2909
+ }
2910
+ function readCurrentKnowledgeGraph(projectDir, codeGraph, expectedInputHash) {
2911
+ const path = (0, node_path_1.join)(graphDir(projectDir), "graph.json");
2912
+ if (!(0, node_fs_1.existsSync)(path))
2913
+ return null;
2914
+ try {
2915
+ const graph = readJson(path);
2916
+ const inputHash = expectedInputHash ?? knowledgeGraphInputHash(projectDir, codeGraph.repo_state.input_hash ?? codeGraphInputHash(projectDir));
2917
+ if (graph.repo_state?.input_hash !== inputHash)
2918
+ return null;
2919
+ return graph;
2920
+ }
2921
+ catch {
2922
+ return null;
2923
+ }
2924
+ }
2925
+ function graphFastFingerprint(projectDir, selection = codeIndexSelection(projectDir)) {
2926
+ const packetPaths = (0, node_fs_1.existsSync)(packetsDir(projectDir))
2927
+ ? (0, node_fs_1.readdirSync)(packetsDir(projectDir))
2928
+ .filter((name) => name.endsWith(".json"))
2929
+ .map((name) => (0, node_path_1.join)(packetsDir(projectDir), name))
2930
+ : [];
2931
+ const paths = [
2932
+ ...selection.files,
2933
+ ...externalIndexFiles(projectDir).map((index) => index.path),
2934
+ ...packetPaths,
2935
+ ];
2936
+ const entries = paths
2937
+ .filter((path) => (0, node_fs_1.existsSync)(path))
2938
+ .map((path) => {
2939
+ const stats = (0, node_fs_1.statSync)(path);
2940
+ return `${projectRelative(projectDir, path)}:${stats.size}:${Math.round(stats.mtimeMs)}`;
2941
+ })
2942
+ .sort();
2943
+ return sha256Hex(entries.join("\n"));
2944
+ }
2945
+ function readCurrentGraphs(projectDir) {
2946
+ const selection = codeIndexSelection(projectDir);
2947
+ const fingerprint = graphFastFingerprint(projectDir, selection);
2948
+ const cacheKey = (0, node_path_1.resolve)(projectDir);
2949
+ const cached = graphMemoryCache.get(cacheKey);
2950
+ if (cached?.fingerprint === fingerprint) {
2951
+ return { codeGraph: cached.codeGraph, knowledgeGraph: cached.knowledgeGraph };
2952
+ }
2953
+ const codeInputHash = codeGraphInputHash(projectDir, selection.files);
2954
+ const knowledgeInputHash = knowledgeGraphInputHash(projectDir, codeInputHash);
2955
+ if (cached?.codeInputHash === codeInputHash && cached.knowledgeInputHash === knowledgeInputHash) {
2956
+ cached.fingerprint = fingerprint;
2957
+ return { codeGraph: cached.codeGraph, knowledgeGraph: cached.knowledgeGraph };
2958
+ }
2959
+ const codeGraph = readCurrentCodeGraph(projectDir, codeInputHash);
2960
+ if (!codeGraph)
2961
+ return null;
2962
+ const knowledgeGraph = readCurrentKnowledgeGraph(projectDir, codeGraph, knowledgeInputHash);
2963
+ if (!knowledgeGraph)
2964
+ return null;
2965
+ graphMemoryCache.set(cacheKey, { fingerprint, codeInputHash, knowledgeInputHash, codeGraph, knowledgeGraph });
2966
+ return { codeGraph, knowledgeGraph };
2967
+ }
2968
+ function currentOrBuildGraphs(projectDir) {
2969
+ const current = readCurrentGraphs(projectDir);
2970
+ if (current?.codeGraph && current.knowledgeGraph) {
2971
+ return {
2972
+ indexes: [
2973
+ (0, node_path_1.join)(indexesDir(projectDir), "catalog.json"),
2974
+ (0, node_path_1.join)(indexesDir(projectDir), "by-path.json"),
2975
+ (0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
2976
+ (0, node_path_1.join)(indexesDir(projectDir), "by-type.json"),
2977
+ (0, node_path_1.join)(indexesDir(projectDir), "graph.json"),
2978
+ (0, node_path_1.join)(indexesDir(projectDir), "code-graph.json"),
2979
+ ],
2980
+ codeGraph: current.codeGraph,
2981
+ knowledgeGraph: current.knowledgeGraph,
2982
+ };
2983
+ }
2984
+ return buildGraphIndexes(projectDir);
2985
+ }
2986
+ function buildGraphIndexes(projectDir) {
2987
+ const written = buildPacketIndexes(projectDir);
2988
+ const codeGraph = buildCodeGraph(projectDir);
2989
+ const knowledgeGraph = buildKnowledgeGraph(projectDir, codeGraph);
2990
+ const graphIndexPath = (0, node_path_1.join)(indexesDir(projectDir), "graph.json");
2991
+ const codeGraphIndexPath = (0, node_path_1.join)(indexesDir(projectDir), "code-graph.json");
2992
+ writeJson(graphIndexPath, {
2655
2993
  schema_version: knowledgeGraph.schema_version,
2656
2994
  entities: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(graphDir(projectDir), "entities.json")),
2657
2995
  edges: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(graphDir(projectDir), "edges.json")),
@@ -2660,7 +2998,7 @@ function buildIndexes(projectDir) {
2660
2998
  edge_count: knowledgeGraph.edges.length,
2661
2999
  episode_count: knowledgeGraph.episodes.length,
2662
3000
  });
2663
- writeJson(written[5], {
3001
+ writeJson(codeGraphIndexPath, {
2664
3002
  schema_version: codeGraph.schema_version,
2665
3003
  files: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(codeGraphDir(projectDir), "files.json")),
2666
3004
  symbols: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(codeGraphDir(projectDir), "symbols.json")),
@@ -2676,9 +3014,23 @@ function buildIndexes(projectDir) {
2676
3014
  route_count: codeGraph.routes.length,
2677
3015
  test_count: codeGraph.tests.length,
2678
3016
  });
2679
- return written;
3017
+ graphMemoryCache.set((0, node_path_1.resolve)(projectDir), {
3018
+ fingerprint: graphFastFingerprint(projectDir),
3019
+ codeInputHash: codeGraph.repo_state.input_hash ?? "",
3020
+ knowledgeInputHash: knowledgeGraph.repo_state.input_hash ?? "",
3021
+ codeGraph,
3022
+ knowledgeGraph,
3023
+ });
3024
+ return {
3025
+ indexes: [...written, graphIndexPath, codeGraphIndexPath],
3026
+ codeGraph,
3027
+ knowledgeGraph,
3028
+ };
3029
+ }
3030
+ function buildIndexes(projectDir) {
3031
+ return buildGraphIndexes(projectDir).indexes;
2680
3032
  }
2681
- function indexProject(projectDir) {
3033
+ function indexProjectDetailed(projectDir, options = {}) {
2682
3034
  ensureMemoryDirs(projectDir);
2683
3035
  const policy = installAgentPolicy(projectDir);
2684
3036
  const migrated = migrateLegacyMarkdown(projectDir);
@@ -2688,15 +3040,23 @@ function indexProject(projectDir) {
2688
3040
  const structure = createRepoStructurePacket(projectDir);
2689
3041
  if (structure)
2690
3042
  upsertGeneratedPacket(projectDir, structure);
2691
- const indexes = buildIndexes(projectDir);
3043
+ const built = options.graphs === false ? null : buildGraphIndexes(projectDir);
3044
+ const indexes = built?.indexes ?? buildPacketIndexes(projectDir);
2692
3045
  return {
2693
- projectDir,
2694
- packets: loadPacketsFromDir(packetsDir(projectDir)).length,
2695
- migrated,
2696
- indexes: indexes.map((path) => (0, node_path_1.relative)(projectDir, path)),
2697
- policyPath: (0, node_path_1.relative)(projectDir, policy.path),
3046
+ result: {
3047
+ projectDir,
3048
+ packets: loadPacketsFromDir(packetsDir(projectDir)).length,
3049
+ migrated,
3050
+ indexes: indexes.map((path) => (0, node_path_1.relative)(projectDir, path)),
3051
+ policyPath: (0, node_path_1.relative)(projectDir, policy.path),
3052
+ },
3053
+ codeGraph: built?.codeGraph,
3054
+ knowledgeGraph: built?.knowledgeGraph,
2698
3055
  };
2699
3056
  }
3057
+ function indexProject(projectDir, options = {}) {
3058
+ return indexProjectDetailed(projectDir, options).result;
3059
+ }
2700
3060
  function staleSuggestedAction(reasons) {
2701
3061
  if (reasons.some((reason) => reason.includes("status is")))
2702
3062
  return "mark_stale";
@@ -2755,11 +3115,20 @@ function refreshPacketStaleness(projectDir) {
2755
3115
  return { findings, updated };
2756
3116
  }
2757
3117
  function refreshProject(projectDir) {
2758
- const index = indexProject(projectDir);
3118
+ const detailedIndex = indexProjectDetailed(projectDir);
3119
+ const index = detailedIndex.result;
3120
+ let codeGraph = detailedIndex.codeGraph;
3121
+ let knowledgeGraph = detailedIndex.knowledgeGraph;
2759
3122
  const stale = refreshPacketStaleness(projectDir);
2760
- const indexes = stale.updated > 0 ? buildIndexes(projectDir).map((path) => (0, node_path_1.relative)(projectDir, path)) : index.indexes;
3123
+ let indexes = index.indexes;
3124
+ if (stale.updated > 0) {
3125
+ const rebuilt = buildGraphIndexes(projectDir);
3126
+ codeGraph = rebuilt.codeGraph;
3127
+ knowledgeGraph = rebuilt.knowledgeGraph;
3128
+ indexes = rebuilt.indexes.map((path) => (0, node_path_1.relative)(projectDir, path));
3129
+ }
2761
3130
  const validation = validateProject(projectDir);
2762
- const metrics = kageMetrics(projectDir);
3131
+ const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph, validation });
2763
3132
  const nextActions = [];
2764
3133
  if (stale.findings.length)
2765
3134
  nextActions.push("Update, verify, or supersede stale repo memories before relying on them.");
@@ -2833,9 +3202,8 @@ function gcProject(projectDir, options = {}) {
2833
3202
  }
2834
3203
  }
2835
3204
  if (!options.dryRun && (deprecated.length || deleted.length)) {
2836
- buildIndexes(projectDir);
2837
- buildKnowledgeGraph(projectDir);
2838
- writeJson((0, node_path_1.join)(memoryRoot(projectDir), "metrics.json"), kageMetrics(projectDir));
3205
+ const rebuilt = buildGraphIndexes(projectDir);
3206
+ writeJson((0, node_path_1.join)(memoryRoot(projectDir), "metrics.json"), kageMetricsShallow(projectDir, rebuilt));
2839
3207
  }
2840
3208
  return {
2841
3209
  ok: true,
@@ -3084,11 +3452,29 @@ function recallIntentBoost(queryTerms, packet) {
3084
3452
  score += packet.type === "decision" ? 12 : 0;
3085
3453
  return score;
3086
3454
  }
3087
- function recallBreakdown(projectDir, terms, packet, textScore) {
3088
- const graph = buildKnowledgeGraph(projectDir);
3089
- const packetEntityId = graph.entities.find((entity) => entity.type === "memory" && entity.aliases.includes(packet.id))?.id;
3455
+ function recallGraphLookup(graph) {
3456
+ const packetEntityByPacketId = new Map();
3457
+ for (const entity of graph.entities) {
3458
+ if (entity.type !== "memory")
3459
+ continue;
3460
+ for (const alias of entity.aliases)
3461
+ packetEntityByPacketId.set(alias, entity.id);
3462
+ }
3463
+ const edgesByEntityId = new Map();
3464
+ for (const edge of graph.edges) {
3465
+ const from = edgesByEntityId.get(edge.from) ?? [];
3466
+ from.push(edge);
3467
+ edgesByEntityId.set(edge.from, from);
3468
+ const to = edgesByEntityId.get(edge.to) ?? [];
3469
+ to.push(edge);
3470
+ edgesByEntityId.set(edge.to, to);
3471
+ }
3472
+ return { packetEntityByPacketId, edgesByEntityId };
3473
+ }
3474
+ function recallBreakdown(projectDir, terms, packet, textScore, graph = buildKnowledgeGraph(projectDir), lookup = recallGraphLookup(graph)) {
3475
+ const packetEntityId = lookup.packetEntityByPacketId.get(packet.id);
3090
3476
  const rawGraphScore = packetEntityId
3091
- ? graph.edges.filter((edge) => edge.from === packetEntityId || edge.to === packetEntityId).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
3477
+ ? (lookup.edgesByEntityId.get(packetEntityId) ?? []).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
3092
3478
  : 0;
3093
3479
  const graphScore = Math.min(rawGraphScore * 0.45, textScore > 0 ? textScore * 1.5 + 12 : 8);
3094
3480
  const pathTypeTag = scoreText(terms, `${packet.type} ${packet.tags.join(" ")} ${packet.paths.join(" ")}`, [packet.type, ...packet.tags, ...packet.paths]);
@@ -3100,15 +3486,19 @@ function recallBreakdown(projectDir, terms, packet, textScore) {
3100
3486
  const final = Number((textScore + graphScore + pathTypeTag * 0.8 + intent + vector + freshness + quality + feedback).toFixed(2));
3101
3487
  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
3488
  }
3103
- function recall(projectDir, query, limit = 5, explain = false) {
3104
- indexProject(projectDir);
3489
+ function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
3490
+ const current = inputs.codeGraph && inputs.knowledgeGraph ? null : readCurrentGraphs(projectDir);
3491
+ const detailedIndex = inputs.codeGraph && inputs.knowledgeGraph || current ? null : indexProjectDetailed(projectDir);
3492
+ const codeGraph = inputs.codeGraph ?? current?.codeGraph ?? detailedIndex?.codeGraph ?? buildCodeGraph(projectDir);
3493
+ const knowledgeGraph = inputs.knowledgeGraph ?? current?.knowledgeGraph ?? detailedIndex?.knowledgeGraph ?? buildKnowledgeGraph(projectDir, codeGraph);
3105
3494
  const terms = tokenize(query);
3106
3495
  const approvedPackets = loadApprovedPackets(projectDir);
3107
3496
  const lexicalScores = scorePacketsBm25(terms, approvedPackets);
3497
+ const graphLookup = recallGraphLookup(knowledgeGraph);
3108
3498
  const scored = approvedPackets
3109
3499
  .map((packet) => {
3110
3500
  const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
3111
- const score_breakdown = recallBreakdown(projectDir, terms, packet, score);
3501
+ const score_breakdown = recallBreakdown(projectDir, terms, packet, score, knowledgeGraph, graphLookup);
3112
3502
  const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
3113
3503
  return { packet, score: score_breakdown.final, relevance, why_matched: why, score_breakdown };
3114
3504
  })
@@ -3134,8 +3524,8 @@ function recall(projectDir, query, limit = 5, explain = false) {
3134
3524
  return true;
3135
3525
  })
3136
3526
  .slice(0, 3);
3137
- const graphContext = queryGraph(projectDir, query, 5);
3138
- const codeContext = queryCodeGraph(projectDir, query, 5);
3527
+ const graphContext = queryGraph(projectDir, query, 5, knowledgeGraph);
3528
+ const codeContext = queryCodeGraph(projectDir, query, 5, codeGraph);
3139
3529
  const lines = [
3140
3530
  `# Kage Context`,
3141
3531
  "",
@@ -3203,8 +3593,8 @@ function scoreText(terms, text, boosts = []) {
3203
3593
  score += 3;
3204
3594
  return score;
3205
3595
  }
3206
- function queryCodeGraph(projectDir, query, limit = 10) {
3207
- const graph = buildCodeGraph(projectDir);
3596
+ function queryCodeGraph(projectDir, query, limit = 10, graph) {
3597
+ graph = graph ?? readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
3208
3598
  const terms = tokenize(query);
3209
3599
  const files = graph.files
3210
3600
  .map((file) => ({ file, score: scoreText(terms, `${file.path} ${file.kind} ${file.language} ${file.parser}`, [file.path, file.language]) }))
@@ -3268,8 +3658,8 @@ function queryCodeGraph(projectDir, query, limit = 10) {
3268
3658
  ];
3269
3659
  return { query, context_block: lines.join("\n"), files, symbols, imports: imports.map((entry) => entry.item), calls, routes, tests };
3270
3660
  }
3271
- function queryGraph(projectDir, query, limit = 10) {
3272
- const graph = buildKnowledgeGraph(projectDir);
3661
+ function queryGraph(projectDir, query, limit = 10, graph) {
3662
+ graph = graph ?? readCurrentGraphs(projectDir)?.knowledgeGraph ?? buildKnowledgeGraph(projectDir);
3273
3663
  const terms = tokenize(query);
3274
3664
  const entityScores = new Map();
3275
3665
  for (const entity of graph.entities) {
@@ -3315,7 +3705,7 @@ function mermaidLabel(value) {
3315
3705
  return value.replace(/["\n\r]/g, " ").slice(0, 80);
3316
3706
  }
3317
3707
  function graphMermaid(projectDir, limit = 40) {
3318
- const graph = buildKnowledgeGraph(projectDir);
3708
+ const graph = readCurrentGraphs(projectDir)?.knowledgeGraph ?? buildKnowledgeGraph(projectDir);
3319
3709
  const selectedEdges = graph.edges.slice(0, limit);
3320
3710
  const selectedEntityIds = new Set(selectedEdges.flatMap((edge) => [edge.from, edge.to]));
3321
3711
  const selectedEntities = graph.entities.filter((entity) => selectedEntityIds.has(entity.id));
@@ -3339,17 +3729,19 @@ function percent(numerator, denominator) {
3339
3729
  }
3340
3730
  function kageMetrics(projectDir) {
3341
3731
  ensureMemoryDirs(projectDir);
3342
- const codeGraph = buildCodeGraph(projectDir);
3343
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
3732
+ const built = currentOrBuildGraphs(projectDir);
3733
+ const codeGraph = built.codeGraph;
3734
+ const knowledgeGraph = built.knowledgeGraph;
3344
3735
  const validation = validateProject(projectDir);
3345
3736
  const approvedPackets = loadPacketsFromDir(packetsDir(projectDir)).length;
3346
3737
  const pendingPackets = loadPacketsFromDir(pendingDir(projectDir)).length;
3347
3738
  const evidenceBackedEdges = knowledgeGraph.edges.filter((edge) => edge.evidence.length > 0).length;
3348
3739
  const policyPath = (0, node_path_1.join)(projectDir, "AGENTS.md");
3349
3740
  const policyInstalled = (0, node_fs_1.existsSync)(policyPath) && (0, node_fs_1.readFileSync)(policyPath, "utf8").includes(AGENTS_POLICY_MARKER);
3741
+ const indexManifest = readCodeIndexManifest(projectDir);
3350
3742
  const sourceFiles = codeGraph.files.filter((file) => file.kind === "source" || file.kind === "test");
3351
3743
  const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
3352
- const coverage = percent(indexedSourceFiles.length, sourceFiles.length);
3744
+ const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
3353
3745
  const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
3354
3746
  const qualityScores = allPackets
3355
3747
  .map((packet) => Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score))
@@ -3371,7 +3763,7 @@ function kageMetrics(projectDir) {
3371
3763
  (validation.ok ? 5 : -20) -
3372
3764
  validation.warnings.length * 2)));
3373
3765
  const quality = qualityReport(projectDir);
3374
- const benchmark = benchmarkProject(projectDir);
3766
+ const benchmark = benchmarkProject(projectDir, { codeGraph, knowledgeGraph });
3375
3767
  return {
3376
3768
  schema_version: 1,
3377
3769
  project_dir: projectDir,
@@ -3389,6 +3781,13 @@ function kageMetrics(projectDir) {
3389
3781
  parsers: countBy(codeGraph.files, (file) => file.parser),
3390
3782
  source_symbols_by_parser: countBy(codeGraph.symbols, (symbol) => symbol.parser),
3391
3783
  indexer_coverage_percent: coverage,
3784
+ index_status: indexManifest.coverage.complete ? "complete" : "partial",
3785
+ indexable_files: indexManifest.coverage.indexable_files || sourceFiles.length,
3786
+ indexed_files: indexManifest.coverage.indexed_files || indexedSourceFiles.length,
3787
+ deferred_files: indexManifest.coverage.deferred_files,
3788
+ ignored_files: indexManifest.coverage.ignored_files,
3789
+ cache_hits: indexManifest.cache.hits,
3790
+ cache_misses: indexManifest.cache.misses,
3392
3791
  },
3393
3792
  memory_graph: {
3394
3793
  approved_packets: approvedPackets,
@@ -3431,8 +3830,9 @@ function auditProject(projectDir) {
3431
3830
  ensureMemoryDirs(projectDir);
3432
3831
  const validation = validateProject(projectDir);
3433
3832
  const quality = qualityReport(projectDir);
3434
- const codeGraph = buildCodeGraph(projectDir);
3435
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
3833
+ const built = currentOrBuildGraphs(projectDir);
3834
+ const codeGraph = built.codeGraph;
3835
+ const knowledgeGraph = built.knowledgeGraph;
3436
3836
  const approved = loadApprovedPackets(projectDir);
3437
3837
  const pending = loadPendingPackets(projectDir);
3438
3838
  const structuredPackets = approved.filter(hasStructuredEngineeringContext);
@@ -3681,8 +4081,11 @@ function qualityReport(projectDir) {
3681
4081
  packets: rows,
3682
4082
  };
3683
4083
  }
3684
- function benchmarkProject(projectDir) {
4084
+ function benchmarkProject(projectDir, inputs = {}) {
3685
4085
  ensureMemoryDirs(projectDir);
4086
+ const built = inputs.codeGraph && inputs.knowledgeGraph ? null : currentOrBuildGraphs(projectDir);
4087
+ const codeGraph = inputs.codeGraph ?? built?.codeGraph;
4088
+ const knowledgeGraph = inputs.knowledgeGraph ?? built?.knowledgeGraph;
3686
4089
  const scenarios = [
3687
4090
  { query: "how do I run tests", expected: "test" },
3688
4091
  { query: "where are routes defined", expected: "route" },
@@ -3690,7 +4093,7 @@ function benchmarkProject(projectDir) {
3690
4093
  { query: "what changed on this branch", expected: "branch" },
3691
4094
  { query: "what gotchas exist", expected: "gotcha" },
3692
4095
  ].map((scenario) => {
3693
- const result = recall(projectDir, scenario.query, 5, true);
4096
+ const result = recall(projectDir, scenario.query, 5, true, { codeGraph, knowledgeGraph });
3694
4097
  const text = `${result.context_block}\n${result.results.map((entry) => packetText(entry.packet)).join("\n")}`.toLowerCase();
3695
4098
  return {
3696
4099
  query: scenario.query,
@@ -3701,7 +4104,7 @@ function benchmarkProject(projectDir) {
3701
4104
  context_tokens: estimateTokens(result.context_block),
3702
4105
  };
3703
4106
  });
3704
- const metrics = kageMetricsShallow(projectDir);
4107
+ const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph });
3705
4108
  const quality = qualityReport(projectDir);
3706
4109
  const typeCoverage = quality.memory_type_coverage;
3707
4110
  const recallHitRate = percent(scenarios.filter((scenario) => scenario.hit).length, scenarios.length);
@@ -3865,13 +4268,14 @@ function benchmarkTaskComparison(projectDir, task) {
3865
4268
  ],
3866
4269
  };
3867
4270
  }
3868
- function kageMetricsShallow(projectDir) {
3869
- const codeGraph = buildCodeGraph(projectDir);
3870
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
3871
- const validation = validateProject(projectDir);
4271
+ function kageMetricsShallow(projectDir, inputs = {}) {
4272
+ const codeGraph = inputs.codeGraph ?? buildCodeGraph(projectDir);
4273
+ const knowledgeGraph = inputs.knowledgeGraph ?? buildKnowledgeGraph(projectDir, codeGraph);
4274
+ const validation = inputs.validation ?? validateProject(projectDir);
4275
+ const indexManifest = readCodeIndexManifest(projectDir);
3872
4276
  const sourceFiles = codeGraph.files.filter((file) => file.kind === "source" || file.kind === "test");
3873
4277
  const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
3874
- const coverage = percent(indexedSourceFiles.length, sourceFiles.length);
4278
+ const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
3875
4279
  const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
3876
4280
  const indexedSourceTokens = Math.ceil(sourceFiles.reduce((sum, file) => sum + file.size_bytes, 0) / 4);
3877
4281
  const memoryTokens = allPackets.reduce((sum, packet) => sum + estimateTokens(packetText(packet)), 0);
@@ -3893,6 +4297,13 @@ function kageMetricsShallow(projectDir) {
3893
4297
  parsers: countBy(codeGraph.files, (file) => file.parser),
3894
4298
  source_symbols_by_parser: countBy(codeGraph.symbols, (symbol) => symbol.parser),
3895
4299
  indexer_coverage_percent: coverage,
4300
+ index_status: indexManifest.coverage.complete ? "complete" : "partial",
4301
+ indexable_files: indexManifest.coverage.indexable_files || sourceFiles.length,
4302
+ indexed_files: indexManifest.coverage.indexed_files || indexedSourceFiles.length,
4303
+ deferred_files: indexManifest.coverage.deferred_files,
4304
+ ignored_files: indexManifest.coverage.ignored_files,
4305
+ cache_hits: indexManifest.cache.hits,
4306
+ cache_misses: indexManifest.cache.misses,
3896
4307
  },
3897
4308
  memory_graph: {
3898
4309
  approved_packets: loadPacketsFromDir(packetsDir(projectDir)).length,
@@ -5578,9 +5989,9 @@ function installClaudeSettings(projectDir) {
5578
5989
  function initProject(projectDir) {
5579
5990
  installAgentPolicy(projectDir);
5580
5991
  installClaudeSettings(projectDir);
5581
- const index = indexProject(projectDir);
5992
+ const index = indexProject(projectDir, { graphs: false });
5582
5993
  const validation = validateProject(projectDir);
5583
- const sampleRecall = recall(projectDir, "how do I run tests");
5994
+ const sampleRecall = recallFromPackets("how do I run tests", loadApprovedPackets(projectDir), 5, "Repo Memory");
5584
5995
  return { index, validation, sampleRecall };
5585
5996
  }
5586
5997
  function doctorProject(projectDir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
5
5
  "main": "dist/index.js",
6
6
  "files": [
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
- <label>
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="9999">All</option>
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; }