@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 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,184 @@ 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 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, listCodeFiles(projectDir), "code_file"),
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 inputHash = codeGraphInputHash(projectDir);
2157
- const absoluteFiles = listCodeFiles(projectDir);
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 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
- }
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, symbols, symbolByName));
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 buildIndexes(projectDir) {
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
- writeJson(written[4], {
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(written[5], {
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
- return written;
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 indexProject(projectDir) {
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 indexes = buildIndexes(projectDir);
3004
+ const built = options.graphs === false ? null : buildGraphIndexes(projectDir);
3005
+ const indexes = built?.indexes ?? buildPacketIndexes(projectDir);
2692
3006
  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),
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 index = indexProject(projectDir);
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
- const indexes = stale.updated > 0 ? buildIndexes(projectDir).map((path) => (0, node_path_1.relative)(projectDir, path)) : index.indexes;
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 = kageMetrics(projectDir);
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
- buildIndexes(projectDir);
2837
- buildKnowledgeGraph(projectDir);
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 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;
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
- ? graph.edges.filter((edge) => edge.from === packetEntityId || edge.to === packetEntityId).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
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
- indexProject(projectDir);
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
- const graph = buildCodeGraph(projectDir);
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
- const graph = buildKnowledgeGraph(projectDir);
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 codeGraph = buildCodeGraph(projectDir);
3343
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
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 codeGraph = buildCodeGraph(projectDir);
3435
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
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 = recall(projectDir, "how do I run tests");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
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; }