@kage-core/kage-graph-mcp 1.1.16 → 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,25 @@ 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
+
25
+ `1.1.17` publishes content-based graph freshness:
26
+
27
+ - `kage pr check` now uses graph input hashes, so push-only operations and
28
+ empty/same-tree commits do not force another refresh while real source,
29
+ approved-memory, or code-index changes still stale generated graph artifacts.
30
+
12
31
  `1.1.16` fixes the guarded release helper's npm verification step:
13
32
 
14
33
  - exact-version `npm view` checks now retry with backoff after publish so npm
@@ -79,6 +98,10 @@ The script fetches the current branch and blocks if the remote branch is not an
79
98
  ancestor of local `HEAD`, which prevents publishing an npm version from a branch
80
99
  that cannot be pushed cleanly.
81
100
 
101
+ Do not refresh again just because the branch was pushed. Graph freshness is
102
+ based on source, approved memory, and code-index inputs; empty/same-tree commits
103
+ are accepted by `kage pr check`.
104
+
82
105
  ## CLI
83
106
 
84
107
  ```bash
@@ -232,9 +255,10 @@ hashes, git state, audit trust, inbox counts, and metrics readiness. CI, PR, and
232
255
  sync workflows build it after refresh.
233
256
 
234
257
  Use `kage refresh --project <repo>` or the `kage_refresh` MCP tool after
235
- meaningful file changes. Refresh rebuilds indexes, code graph, memory graph,
236
- metrics, and stale-memory metadata. Memory is marked stale when status or
237
- feedback says it is stale, its TTL expires, or grounded paths disappear.
258
+ meaningful file/content changes. Refresh rebuilds indexes, code graph, memory
259
+ graph, metrics, and stale-memory metadata. Memory is marked stale when status or
260
+ feedback says it is stale, its TTL expires, or grounded paths disappear. Pushes
261
+ and empty/same-tree commits do not need another refresh.
238
262
 
239
263
  Use `kage gc --project <repo> --dry-run` to preview stale packet cleanup.
240
264
  `kage gc --project <repo>` marks stale repo packets deprecated, rebuilds
@@ -407,7 +431,7 @@ Minimum policy:
407
431
  Before code changes or repo-specific answers:
408
432
  1. Call `kage_context` with `project_dir` and the user task as `query`.
409
433
  2. Capture reusable learnings with `kage_learn` or `kage_capture`.
410
- 3. After meaningful file changes, call `kage_refresh`.
434
+ 3. After meaningful file/content changes, call `kage_refresh`; skip it for push-only or same-tree commits.
411
435
  4. Before finishing changed-file tasks, call `kage_propose_from_diff` or `kage_pr_summarize`.
412
436
  5. Before merge, call `kage_pr_check`.
413
437
  6. Never publish or promote org/global memory automatically.
package/dist/index.js CHANGED
@@ -217,7 +217,7 @@ function listTools() {
217
217
  },
218
218
  {
219
219
  name: "kage_refresh",
220
- description: "Rebuild repo indexes, code graph, memory graph, metrics, and stale-memory metadata. Agents should run this after meaningful file changes and before PR checks.",
220
+ description: "Rebuild repo indexes, code graph, memory graph, metrics, and stale-memory metadata. Agents should run this after meaningful file/content changes before PR checks; push-only or same-tree commits do not need another refresh.",
221
221
  inputSchema: {
222
222
  type: "object",
223
223
  properties: {
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",
@@ -193,8 +194,10 @@ decisions, debugging, explanation, or action. Do not store raw transcripts.
193
194
 
194
195
  ## End-Of-Task Proposal
195
196
 
196
- After meaningful file changes, call \`kage_refresh\` so indexes, code graph,
197
- memory graph, metrics, and stale-memory checks are current.
197
+ After meaningful file/content changes, call \`kage_refresh\` so indexes, code
198
+ graph, memory graph, metrics, and stale-memory checks are current. Do not
199
+ refresh solely because a branch was pushed, an empty commit was created, or the
200
+ git commit changed without graph inputs changing.
198
201
 
199
202
  Before finishing a task that changed files, call \`kage_pr_summarize\` or
200
203
  \`kage_propose_from_diff\`, then call \`kage_pr_check\`.
@@ -232,7 +235,7 @@ For normal coding tasks:
232
235
  1. \`kage_context\` — validate + recall + code graph + knowledge graph in one call
233
236
  2. Work on the task
234
237
  3. \`kage_learn\` for concrete learnings
235
- 4. \`kage_refresh\` after meaningful file changes
238
+ 4. \`kage_refresh\` after meaningful file/content changes, not after push-only or same-tree commits
236
239
  5. \`kage_propose_from_diff\` before the final response to create repo-local change memory
237
240
 
238
241
  For quick factual questions, \`kage_context\` alone is enough. For status or demo requests, call \`kage_metrics\`.
@@ -882,6 +885,9 @@ function gitBranch(projectDir) {
882
885
  function gitHead(projectDir) {
883
886
  return readGit(projectDir, ["rev-parse", "HEAD"]);
884
887
  }
888
+ function gitTree(projectDir) {
889
+ return readGit(projectDir, ["rev-parse", "HEAD^{tree}"]);
890
+ }
885
891
  function gitMergeBase(projectDir) {
886
892
  return readGit(projectDir, ["merge-base", "HEAD", "origin/main"])
887
893
  || readGit(projectDir, ["merge-base", "HEAD", "origin/master"]);
@@ -1292,6 +1298,11 @@ const CODE_EXTENSIONS = new Set([
1292
1298
  ".hpp",
1293
1299
  ".swift",
1294
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);
1295
1306
  const CONFIG_NAMES = new Set([
1296
1307
  "package.json",
1297
1308
  "pyproject.toml",
@@ -1310,6 +1321,10 @@ const CONFIG_NAMES = new Set([
1310
1321
  "vitest.config.js",
1311
1322
  "vitest.config.ts",
1312
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
+ }
1313
1328
  function extensionOf(path) {
1314
1329
  const match = path.match(/\.[^.\/]+$/);
1315
1330
  return match ? match[0] : "";
@@ -1317,7 +1332,26 @@ function extensionOf(path) {
1317
1332
  function shouldSkipCodePath(relativePath) {
1318
1333
  return relativePath
1319
1334
  .split("/")
1320
- .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));
1321
1355
  }
1322
1356
  function codeLanguage(path) {
1323
1357
  const extension = extensionOf(path);
@@ -1379,14 +1413,184 @@ function codeFileKind(path) {
1379
1413
  return "doc";
1380
1414
  return "source";
1381
1415
  }
1382
- function listCodeFiles(projectDir) {
1383
- 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)) {
1384
1489
  const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
1385
- if (shouldSkipCodePath(rel))
1386
- return false;
1387
- const extension = extensionOf(rel);
1388
- return CODE_EXTENSIONS.has(extension) || CONFIG_NAMES.has((0, node_path_1.basename)(rel)) || rel === "README.md";
1389
- });
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;
1390
1594
  }
1391
1595
  function lineForOffset(text, offset) {
1392
1596
  return text.slice(0, offset).split(/\r?\n/).length;
@@ -1774,6 +1978,8 @@ function extractCalls(path, text, symbols, symbolByName) {
1774
1978
  const sourceFile = sourceFileFor(path, text);
1775
1979
  const calls = [];
1776
1980
  const visit = (node) => {
1981
+ if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
1982
+ return;
1777
1983
  if (!ts.isCallExpression(node)) {
1778
1984
  ts.forEachChild(node, visit);
1779
1985
  return;
@@ -1791,6 +1997,8 @@ function extractCalls(path, text, symbols, symbolByName) {
1791
1997
  const line = lineForNode(sourceFile, node);
1792
1998
  const caller = symbolAtLine(symbols, path, line);
1793
1999
  for (const target of targets.slice(0, 3)) {
2000
+ if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
2001
+ break;
1794
2002
  if (target.path === path && target.line === line)
1795
2003
  continue;
1796
2004
  calls.push({ from_symbol: caller?.id ?? null, to_symbol: target.id, path, line });
@@ -1863,6 +2071,49 @@ function externalIndexFiles(projectDir) {
1863
2071
  { path: (0, node_path_1.join)(projectDir, "dump.lsif"), parser: "lsif", format: "lsif" },
1864
2072
  ];
1865
2073
  }
2074
+ function sha256Hex(content) {
2075
+ return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
2076
+ }
2077
+ function projectRelative(projectDir, path) {
2078
+ return (0, node_path_1.relative)(projectDir, path).replace(/\\/g, "/");
2079
+ }
2080
+ function graphInputHash(entries) {
2081
+ const hash = (0, node_crypto_1.createHash)("sha256");
2082
+ const sorted = entries.slice().sort((a, b) => a.kind.localeCompare(b.kind) || a.path.localeCompare(b.path));
2083
+ for (const entry of sorted) {
2084
+ hash.update(entry.kind);
2085
+ hash.update("\0");
2086
+ hash.update(entry.path);
2087
+ hash.update("\0");
2088
+ hash.update(entry.sha256);
2089
+ hash.update("\0");
2090
+ }
2091
+ return hash.digest("hex");
2092
+ }
2093
+ function fileInputEntries(projectDir, paths, kind) {
2094
+ return paths
2095
+ .filter((path) => (0, node_fs_1.existsSync)(path))
2096
+ .map((path) => ({
2097
+ kind,
2098
+ path: projectRelative(projectDir, path),
2099
+ sha256: sha256Hex((0, node_fs_1.readFileSync)(path)),
2100
+ }));
2101
+ }
2102
+ function codeGraphInputHash(projectDir, absoluteFiles = listCodeFiles(projectDir)) {
2103
+ return graphInputHash([
2104
+ ...fileInputEntries(projectDir, absoluteFiles, "code_file"),
2105
+ ...fileInputEntries(projectDir, externalIndexFiles(projectDir).map((index) => index.path), "external_code_index"),
2106
+ ]);
2107
+ }
2108
+ function knowledgeGraphInputHash(projectDir, codeInputHash = codeGraphInputHash(projectDir)) {
2109
+ const packetEntries = loadPacketEntriesFromDir(packetsDir(projectDir))
2110
+ .filter((entry) => entry.packet.status === "approved")
2111
+ .map((entry) => entry.path);
2112
+ return graphInputHash([
2113
+ { kind: "code_graph_input", path: ".agent_memory/code_graph/input", sha256: codeInputHash },
2114
+ ...fileInputEntries(projectDir, packetEntries, "approved_packet"),
2115
+ ]);
2116
+ }
1866
2117
  function normalizeExternalKind(value) {
1867
2118
  const kind = String(value ?? "").toLowerCase();
1868
2119
  if (["function", "method", "class", "constant", "route", "test"].includes(kind))
@@ -2103,39 +2354,37 @@ function buildCodeGraph(projectDir) {
2103
2354
  ensureMemoryDirs(projectDir);
2104
2355
  const branch = gitBranch(projectDir);
2105
2356
  const head = gitHead(projectDir);
2357
+ const tree = gitTree(projectDir);
2106
2358
  const mergeBase = gitMergeBase(projectDir);
2107
- 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);
2108
2363
  const knownFiles = new Set(absoluteFiles.map((path) => (0, node_path_1.relative)(projectDir, path).replace(/\\/g, "/")));
2109
2364
  const files = [];
2110
2365
  const symbols = [];
2111
2366
  const imports = [];
2112
2367
  const contents = new Map();
2368
+ let cacheHits = 0;
2369
+ let cacheMisses = 0;
2113
2370
  for (const absolutePath of absoluteFiles) {
2114
- const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
2115
- const content = (0, node_fs_1.readFileSync)(absolutePath, "utf8");
2116
- contents.set(rel, content);
2117
- files.push({
2118
- id: `file:${slugify(rel)}`,
2119
- path: rel,
2120
- language: codeLanguage(rel),
2121
- parser: codeParser(rel),
2122
- kind: codeFileKind(rel),
2123
- size_bytes: Buffer.byteLength(content),
2124
- line_count: content.split(/\r?\n/).length,
2125
- hash: (0, node_crypto_1.createHash)("sha256").update(content).digest("hex").slice(0, 16),
2126
- });
2127
- if (TS_AST_EXTENSIONS.has(extensionOf(rel))) {
2128
- symbols.push(...extractSymbols(rel, content));
2129
- imports.push(...extractImports(projectDir, rel, content, knownFiles));
2130
- }
2131
- else if (CODE_EXTENSIONS.has(extensionOf(rel))) {
2132
- symbols.push(...extractGenericSymbols(rel, content));
2133
- imports.push(...extractGenericImports(projectDir, rel, content, knownFiles));
2134
- }
2135
- }
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);
2136
2383
  const externalFacts = loadExternalCodeFacts(projectDir);
2137
2384
  const fileByPath = new Map(files.map((file) => [file.path, file]));
2138
2385
  const addSymbol = (symbol) => {
2386
+ if (symbols.length >= MAX_CODE_GRAPH_SYMBOLS)
2387
+ return;
2139
2388
  if (!fileByPath.has(symbol.path))
2140
2389
  return;
2141
2390
  const file = fileByPath.get(symbol.path);
@@ -2173,13 +2422,17 @@ function buildCodeGraph(projectDir) {
2173
2422
  for (const [rel, content] of contents) {
2174
2423
  if (!TS_AST_EXTENSIONS.has(extensionOf(rel)))
2175
2424
  continue;
2425
+ if (calls.length >= MAX_CODE_GRAPH_CALLS)
2426
+ break;
2176
2427
  const fileSymbols = symbols.filter((symbol) => symbol.path === rel);
2177
2428
  const fileImports = imports.filter((item) => item.from_path === rel);
2178
- 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)));
2179
2430
  routes.push(...extractRoutes(rel, content, fileSymbols));
2180
2431
  tests.push(...extractTests(rel, content, fileSymbols, fileImports));
2181
2432
  }
2182
2433
  for (const call of externalFacts.calls) {
2434
+ if (calls.length >= MAX_CODE_GRAPH_CALLS)
2435
+ break;
2183
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))
2184
2437
  calls.push(call);
2185
2438
  }
@@ -2188,7 +2441,7 @@ function buildCodeGraph(projectDir) {
2188
2441
  project_dir: projectDir,
2189
2442
  repo_key: repoKey(projectDir),
2190
2443
  generated_at: nowIso(),
2191
- repo_state: { branch, head, merge_base: mergeBase },
2444
+ repo_state: { branch, head, merge_base: mergeBase, tree, input_hash: inputHash },
2192
2445
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
2193
2446
  symbols: symbols.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.name.localeCompare(b.name)),
2194
2447
  imports: imports.sort((a, b) => a.from_path.localeCompare(b.from_path) || a.line - b.line || a.specifier.localeCompare(b.specifier)),
@@ -2205,20 +2458,22 @@ function buildCodeGraph(projectDir) {
2205
2458
  writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "tests.json"), graph.tests);
2206
2459
  writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "packages.json"), graph.packages);
2207
2460
  writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "graph.json"), graph);
2461
+ graphMemoryCache.delete((0, node_path_1.resolve)(projectDir));
2208
2462
  return graph;
2209
2463
  }
2210
- function buildKnowledgeGraph(projectDir) {
2464
+ function buildKnowledgeGraph(projectDir, codeGraph = buildCodeGraph(projectDir)) {
2211
2465
  ensureMemoryDirs(projectDir);
2212
2466
  const packets = loadApprovedPackets(projectDir).sort((a, b) => a.id.localeCompare(b.id));
2213
2467
  const branch = gitBranch(projectDir);
2214
2468
  const head = gitHead(projectDir);
2469
+ const tree = gitTree(projectDir);
2215
2470
  const mergeBase = gitMergeBase(projectDir);
2216
2471
  const entities = new Map();
2217
2472
  const edges = new Map();
2218
2473
  const episodes = [];
2219
2474
  const repoEntityId = graphEntityId("repo", repoKey(projectDir));
2220
2475
  const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
2221
- const codeGraph = buildCodeGraph(projectDir);
2476
+ const inputHash = knowledgeGraphInputHash(projectDir, codeGraph.repo_state.input_hash ?? codeGraphInputHash(projectDir));
2222
2477
  addEntity(entities, {
2223
2478
  id: repoEntityId,
2224
2479
  type: "repo",
@@ -2540,7 +2795,7 @@ function buildKnowledgeGraph(projectDir) {
2540
2795
  project_dir: projectDir,
2541
2796
  repo_key: repoKey(projectDir),
2542
2797
  generated_from_updated_at: generatedFrom,
2543
- repo_state: { branch, head, merge_base: mergeBase },
2798
+ repo_state: { branch, head, merge_base: mergeBase, tree, input_hash: inputHash },
2544
2799
  episodes: episodes.sort((a, b) => a.id.localeCompare(b.id)),
2545
2800
  entities: [...entities.values()].sort((a, b) => a.id.localeCompare(b.id)),
2546
2801
  edges: [...edges.values()].sort((a, b) => a.id.localeCompare(b.id)),
@@ -2549,13 +2804,12 @@ function buildKnowledgeGraph(projectDir) {
2549
2804
  writeJson((0, node_path_1.join)(graphDir(projectDir), "entities.json"), graph.entities);
2550
2805
  writeJson((0, node_path_1.join)(graphDir(projectDir), "edges.json"), graph.edges);
2551
2806
  writeJson((0, node_path_1.join)(graphDir(projectDir), "graph.json"), graph);
2807
+ graphMemoryCache.delete((0, node_path_1.resolve)(projectDir));
2552
2808
  return graph;
2553
2809
  }
2554
- function buildIndexes(projectDir) {
2810
+ function buildPacketIndexes(projectDir) {
2555
2811
  ensureMemoryDirs(projectDir);
2556
2812
  const packets = loadPacketsFromDir(packetsDir(projectDir)).sort((a, b) => a.id.localeCompare(b.id));
2557
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
2558
- const codeGraph = buildCodeGraph(projectDir);
2559
2813
  const byPath = {};
2560
2814
  const byTag = {};
2561
2815
  const byType = {};
@@ -2592,14 +2846,111 @@ function buildIndexes(projectDir) {
2592
2846
  (0, node_path_1.join)(indexesDir(projectDir), "by-path.json"),
2593
2847
  (0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
2594
2848
  (0, node_path_1.join)(indexesDir(projectDir), "by-type.json"),
2595
- (0, node_path_1.join)(indexesDir(projectDir), "graph.json"),
2596
- (0, node_path_1.join)(indexesDir(projectDir), "code-graph.json"),
2597
2849
  ];
2598
2850
  writeJson(written[0], catalog);
2599
2851
  writeJson(written[1], byPath);
2600
2852
  writeJson(written[2], byTag);
2601
2853
  writeJson(written[3], byType);
2602
- 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, {
2603
2954
  schema_version: knowledgeGraph.schema_version,
2604
2955
  entities: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(graphDir(projectDir), "entities.json")),
2605
2956
  edges: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(graphDir(projectDir), "edges.json")),
@@ -2608,7 +2959,7 @@ function buildIndexes(projectDir) {
2608
2959
  edge_count: knowledgeGraph.edges.length,
2609
2960
  episode_count: knowledgeGraph.episodes.length,
2610
2961
  });
2611
- writeJson(written[5], {
2962
+ writeJson(codeGraphIndexPath, {
2612
2963
  schema_version: codeGraph.schema_version,
2613
2964
  files: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(codeGraphDir(projectDir), "files.json")),
2614
2965
  symbols: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(codeGraphDir(projectDir), "symbols.json")),
@@ -2624,9 +2975,23 @@ function buildIndexes(projectDir) {
2624
2975
  route_count: codeGraph.routes.length,
2625
2976
  test_count: codeGraph.tests.length,
2626
2977
  });
2627
- 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
+ };
2990
+ }
2991
+ function buildIndexes(projectDir) {
2992
+ return buildGraphIndexes(projectDir).indexes;
2628
2993
  }
2629
- function indexProject(projectDir) {
2994
+ function indexProjectDetailed(projectDir, options = {}) {
2630
2995
  ensureMemoryDirs(projectDir);
2631
2996
  const policy = installAgentPolicy(projectDir);
2632
2997
  const migrated = migrateLegacyMarkdown(projectDir);
@@ -2636,15 +3001,23 @@ function indexProject(projectDir) {
2636
3001
  const structure = createRepoStructurePacket(projectDir);
2637
3002
  if (structure)
2638
3003
  upsertGeneratedPacket(projectDir, structure);
2639
- const indexes = buildIndexes(projectDir);
3004
+ const built = options.graphs === false ? null : buildGraphIndexes(projectDir);
3005
+ const indexes = built?.indexes ?? buildPacketIndexes(projectDir);
2640
3006
  return {
2641
- projectDir,
2642
- packets: loadPacketsFromDir(packetsDir(projectDir)).length,
2643
- migrated,
2644
- indexes: indexes.map((path) => (0, node_path_1.relative)(projectDir, path)),
2645
- 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,
2646
3016
  };
2647
3017
  }
3018
+ function indexProject(projectDir, options = {}) {
3019
+ return indexProjectDetailed(projectDir, options).result;
3020
+ }
2648
3021
  function staleSuggestedAction(reasons) {
2649
3022
  if (reasons.some((reason) => reason.includes("status is")))
2650
3023
  return "mark_stale";
@@ -2703,11 +3076,20 @@ function refreshPacketStaleness(projectDir) {
2703
3076
  return { findings, updated };
2704
3077
  }
2705
3078
  function refreshProject(projectDir) {
2706
- const index = indexProject(projectDir);
3079
+ const detailedIndex = indexProjectDetailed(projectDir);
3080
+ const index = detailedIndex.result;
3081
+ let codeGraph = detailedIndex.codeGraph;
3082
+ let knowledgeGraph = detailedIndex.knowledgeGraph;
2707
3083
  const stale = refreshPacketStaleness(projectDir);
2708
- 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
+ }
2709
3091
  const validation = validateProject(projectDir);
2710
- const metrics = kageMetrics(projectDir);
3092
+ const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph, validation });
2711
3093
  const nextActions = [];
2712
3094
  if (stale.findings.length)
2713
3095
  nextActions.push("Update, verify, or supersede stale repo memories before relying on them.");
@@ -2781,9 +3163,8 @@ function gcProject(projectDir, options = {}) {
2781
3163
  }
2782
3164
  }
2783
3165
  if (!options.dryRun && (deprecated.length || deleted.length)) {
2784
- buildIndexes(projectDir);
2785
- buildKnowledgeGraph(projectDir);
2786
- 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));
2787
3168
  }
2788
3169
  return {
2789
3170
  ok: true,
@@ -3032,11 +3413,29 @@ function recallIntentBoost(queryTerms, packet) {
3032
3413
  score += packet.type === "decision" ? 12 : 0;
3033
3414
  return score;
3034
3415
  }
3035
- function recallBreakdown(projectDir, terms, packet, textScore) {
3036
- const graph = buildKnowledgeGraph(projectDir);
3037
- 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);
3038
3437
  const rawGraphScore = packetEntityId
3039
- ? 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)
3040
3439
  : 0;
3041
3440
  const graphScore = Math.min(rawGraphScore * 0.45, textScore > 0 ? textScore * 1.5 + 12 : 8);
3042
3441
  const pathTypeTag = scoreText(terms, `${packet.type} ${packet.tags.join(" ")} ${packet.paths.join(" ")}`, [packet.type, ...packet.tags, ...packet.paths]);
@@ -3048,15 +3447,19 @@ function recallBreakdown(projectDir, terms, packet, textScore) {
3048
3447
  const final = Number((textScore + graphScore + pathTypeTag * 0.8 + intent + vector + freshness + quality + feedback).toFixed(2));
3049
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 };
3050
3449
  }
3051
- function recall(projectDir, query, limit = 5, explain = false) {
3052
- 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);
3053
3455
  const terms = tokenize(query);
3054
3456
  const approvedPackets = loadApprovedPackets(projectDir);
3055
3457
  const lexicalScores = scorePacketsBm25(terms, approvedPackets);
3458
+ const graphLookup = recallGraphLookup(knowledgeGraph);
3056
3459
  const scored = approvedPackets
3057
3460
  .map((packet) => {
3058
3461
  const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
3059
- const score_breakdown = recallBreakdown(projectDir, terms, packet, score);
3462
+ const score_breakdown = recallBreakdown(projectDir, terms, packet, score, knowledgeGraph, graphLookup);
3060
3463
  const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
3061
3464
  return { packet, score: score_breakdown.final, relevance, why_matched: why, score_breakdown };
3062
3465
  })
@@ -3082,8 +3485,8 @@ function recall(projectDir, query, limit = 5, explain = false) {
3082
3485
  return true;
3083
3486
  })
3084
3487
  .slice(0, 3);
3085
- const graphContext = queryGraph(projectDir, query, 5);
3086
- const codeContext = queryCodeGraph(projectDir, query, 5);
3488
+ const graphContext = queryGraph(projectDir, query, 5, knowledgeGraph);
3489
+ const codeContext = queryCodeGraph(projectDir, query, 5, codeGraph);
3087
3490
  const lines = [
3088
3491
  `# Kage Context`,
3089
3492
  "",
@@ -3151,8 +3554,8 @@ function scoreText(terms, text, boosts = []) {
3151
3554
  score += 3;
3152
3555
  return score;
3153
3556
  }
3154
- function queryCodeGraph(projectDir, query, limit = 10) {
3155
- const graph = buildCodeGraph(projectDir);
3557
+ function queryCodeGraph(projectDir, query, limit = 10, graph) {
3558
+ graph = graph ?? readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
3156
3559
  const terms = tokenize(query);
3157
3560
  const files = graph.files
3158
3561
  .map((file) => ({ file, score: scoreText(terms, `${file.path} ${file.kind} ${file.language} ${file.parser}`, [file.path, file.language]) }))
@@ -3216,8 +3619,8 @@ function queryCodeGraph(projectDir, query, limit = 10) {
3216
3619
  ];
3217
3620
  return { query, context_block: lines.join("\n"), files, symbols, imports: imports.map((entry) => entry.item), calls, routes, tests };
3218
3621
  }
3219
- function queryGraph(projectDir, query, limit = 10) {
3220
- const graph = buildKnowledgeGraph(projectDir);
3622
+ function queryGraph(projectDir, query, limit = 10, graph) {
3623
+ graph = graph ?? readCurrentGraphs(projectDir)?.knowledgeGraph ?? buildKnowledgeGraph(projectDir);
3221
3624
  const terms = tokenize(query);
3222
3625
  const entityScores = new Map();
3223
3626
  for (const entity of graph.entities) {
@@ -3263,7 +3666,7 @@ function mermaidLabel(value) {
3263
3666
  return value.replace(/["\n\r]/g, " ").slice(0, 80);
3264
3667
  }
3265
3668
  function graphMermaid(projectDir, limit = 40) {
3266
- const graph = buildKnowledgeGraph(projectDir);
3669
+ const graph = readCurrentGraphs(projectDir)?.knowledgeGraph ?? buildKnowledgeGraph(projectDir);
3267
3670
  const selectedEdges = graph.edges.slice(0, limit);
3268
3671
  const selectedEntityIds = new Set(selectedEdges.flatMap((edge) => [edge.from, edge.to]));
3269
3672
  const selectedEntities = graph.entities.filter((entity) => selectedEntityIds.has(entity.id));
@@ -3287,17 +3690,19 @@ function percent(numerator, denominator) {
3287
3690
  }
3288
3691
  function kageMetrics(projectDir) {
3289
3692
  ensureMemoryDirs(projectDir);
3290
- const codeGraph = buildCodeGraph(projectDir);
3291
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
3693
+ const built = currentOrBuildGraphs(projectDir);
3694
+ const codeGraph = built.codeGraph;
3695
+ const knowledgeGraph = built.knowledgeGraph;
3292
3696
  const validation = validateProject(projectDir);
3293
3697
  const approvedPackets = loadPacketsFromDir(packetsDir(projectDir)).length;
3294
3698
  const pendingPackets = loadPacketsFromDir(pendingDir(projectDir)).length;
3295
3699
  const evidenceBackedEdges = knowledgeGraph.edges.filter((edge) => edge.evidence.length > 0).length;
3296
3700
  const policyPath = (0, node_path_1.join)(projectDir, "AGENTS.md");
3297
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);
3298
3703
  const sourceFiles = codeGraph.files.filter((file) => file.kind === "source" || file.kind === "test");
3299
3704
  const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
3300
- const coverage = percent(indexedSourceFiles.length, sourceFiles.length);
3705
+ const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
3301
3706
  const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
3302
3707
  const qualityScores = allPackets
3303
3708
  .map((packet) => Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score))
@@ -3319,7 +3724,7 @@ function kageMetrics(projectDir) {
3319
3724
  (validation.ok ? 5 : -20) -
3320
3725
  validation.warnings.length * 2)));
3321
3726
  const quality = qualityReport(projectDir);
3322
- const benchmark = benchmarkProject(projectDir);
3727
+ const benchmark = benchmarkProject(projectDir, { codeGraph, knowledgeGraph });
3323
3728
  return {
3324
3729
  schema_version: 1,
3325
3730
  project_dir: projectDir,
@@ -3337,6 +3742,13 @@ function kageMetrics(projectDir) {
3337
3742
  parsers: countBy(codeGraph.files, (file) => file.parser),
3338
3743
  source_symbols_by_parser: countBy(codeGraph.symbols, (symbol) => symbol.parser),
3339
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,
3340
3752
  },
3341
3753
  memory_graph: {
3342
3754
  approved_packets: approvedPackets,
@@ -3379,8 +3791,9 @@ function auditProject(projectDir) {
3379
3791
  ensureMemoryDirs(projectDir);
3380
3792
  const validation = validateProject(projectDir);
3381
3793
  const quality = qualityReport(projectDir);
3382
- const codeGraph = buildCodeGraph(projectDir);
3383
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
3794
+ const built = currentOrBuildGraphs(projectDir);
3795
+ const codeGraph = built.codeGraph;
3796
+ const knowledgeGraph = built.knowledgeGraph;
3384
3797
  const approved = loadApprovedPackets(projectDir);
3385
3798
  const pending = loadPendingPackets(projectDir);
3386
3799
  const structuredPackets = approved.filter(hasStructuredEngineeringContext);
@@ -3629,8 +4042,11 @@ function qualityReport(projectDir) {
3629
4042
  packets: rows,
3630
4043
  };
3631
4044
  }
3632
- function benchmarkProject(projectDir) {
4045
+ function benchmarkProject(projectDir, inputs = {}) {
3633
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;
3634
4050
  const scenarios = [
3635
4051
  { query: "how do I run tests", expected: "test" },
3636
4052
  { query: "where are routes defined", expected: "route" },
@@ -3638,7 +4054,7 @@ function benchmarkProject(projectDir) {
3638
4054
  { query: "what changed on this branch", expected: "branch" },
3639
4055
  { query: "what gotchas exist", expected: "gotcha" },
3640
4056
  ].map((scenario) => {
3641
- const result = recall(projectDir, scenario.query, 5, true);
4057
+ const result = recall(projectDir, scenario.query, 5, true, { codeGraph, knowledgeGraph });
3642
4058
  const text = `${result.context_block}\n${result.results.map((entry) => packetText(entry.packet)).join("\n")}`.toLowerCase();
3643
4059
  return {
3644
4060
  query: scenario.query,
@@ -3649,7 +4065,7 @@ function benchmarkProject(projectDir) {
3649
4065
  context_tokens: estimateTokens(result.context_block),
3650
4066
  };
3651
4067
  });
3652
- const metrics = kageMetricsShallow(projectDir);
4068
+ const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph });
3653
4069
  const quality = qualityReport(projectDir);
3654
4070
  const typeCoverage = quality.memory_type_coverage;
3655
4071
  const recallHitRate = percent(scenarios.filter((scenario) => scenario.hit).length, scenarios.length);
@@ -3813,13 +4229,14 @@ function benchmarkTaskComparison(projectDir, task) {
3813
4229
  ],
3814
4230
  };
3815
4231
  }
3816
- function kageMetricsShallow(projectDir) {
3817
- const codeGraph = buildCodeGraph(projectDir);
3818
- const knowledgeGraph = buildKnowledgeGraph(projectDir);
3819
- 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);
3820
4237
  const sourceFiles = codeGraph.files.filter((file) => file.kind === "source" || file.kind === "test");
3821
4238
  const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
3822
- const coverage = percent(indexedSourceFiles.length, sourceFiles.length);
4239
+ const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
3823
4240
  const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
3824
4241
  const indexedSourceTokens = Math.ceil(sourceFiles.reduce((sum, file) => sum + file.size_bytes, 0) / 4);
3825
4242
  const memoryTokens = allPackets.reduce((sum, packet) => sum + estimateTokens(packetText(packet)), 0);
@@ -3841,6 +4258,13 @@ function kageMetricsShallow(projectDir) {
3841
4258
  parsers: countBy(codeGraph.files, (file) => file.parser),
3842
4259
  source_symbols_by_parser: countBy(codeGraph.symbols, (symbol) => symbol.parser),
3843
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,
3844
4268
  },
3845
4269
  memory_graph: {
3846
4270
  approved_packets: loadPacketsFromDir(packetsDir(projectDir)).length,
@@ -4239,7 +4663,7 @@ Before making code changes or answering implementation questions:
4239
4663
  1. Call kage_context with project_dir and the user task as query.
4240
4664
  2. Use returned memory only when it is relevant, source-backed, and not stale.
4241
4665
  When you learn something reusable: kage_learn.
4242
- After meaningful file changes: kage_refresh.
4666
+ After meaningful file/content changes: kage_refresh. Push-only or same-tree commits do not need another refresh.
4243
4667
  Before finishing a task that changed files: kage_pr_summarize or kage_propose_from_diff, then kage_pr_check.
4244
4668
  If recalled memory helped: kage_feedback helpful. If wrong or stale: kage_feedback wrong or stale."
4245
4669
  fi
@@ -4963,15 +5387,22 @@ function createReviewArtifact(projectDir) {
4963
5387
  (0, node_fs_1.writeFileSync)(path, `${lines.join("\n").trim()}\n`, "utf8");
4964
5388
  return { path, pending: pending.length };
4965
5389
  }
4966
- function graphIsCurrent(projectDir, relativePath, head) {
5390
+ function graphIsCurrent(projectDir, relativePath, expected) {
4967
5391
  const path = (0, node_path_1.join)(projectDir, relativePath);
4968
5392
  if (!(0, node_fs_1.existsSync)(path))
4969
5393
  return false;
4970
- if (!head)
4971
- return true;
4972
5394
  try {
4973
5395
  const graph = readJson(path);
4974
- return graph.repo_state?.head === head;
5396
+ const repoState = graph.repo_state;
5397
+ if (!repoState)
5398
+ return false;
5399
+ if (expected.inputHash && repoState.input_hash)
5400
+ return repoState.input_hash === expected.inputHash;
5401
+ if (expected.tree && repoState.tree)
5402
+ return repoState.tree === expected.tree;
5403
+ if (!expected.head)
5404
+ return true;
5405
+ return repoState.head === expected.head;
4975
5406
  }
4976
5407
  catch {
4977
5408
  return false;
@@ -5005,6 +5436,9 @@ function prCheck(projectDir) {
5005
5436
  const overlay = buildBranchOverlay(projectDir);
5006
5437
  const rawStatus = readGit(projectDir, ["status", "--porcelain", "-uall"]) ?? "";
5007
5438
  const validation = validateProject(projectDir);
5439
+ const tree = gitTree(projectDir);
5440
+ const codeInputHash = codeGraphInputHash(projectDir);
5441
+ const memoryInputHash = knowledgeGraphInputHash(projectDir, codeInputHash);
5008
5442
  const stalePackets = loadPacketsFromDir(packetsDir(projectDir))
5009
5443
  .map((packet) => ({ packet, reasons: staleMemoryReasons(projectDir, packet) }))
5010
5444
  .filter((entry) => entry.reasons.length)
@@ -5014,8 +5448,8 @@ function prCheck(projectDir) {
5014
5448
  .map(parsePorcelainPath)
5015
5449
  .map((path) => path.replace(/^.* -> /, ""))
5016
5450
  .filter((path) => path.startsWith(".agent_memory/packets/") && path.endsWith(".json"))).sort();
5017
- const codeGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/code_graph/graph.json", overlay.head);
5018
- const memoryGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/graph/graph.json", overlay.head);
5451
+ const codeGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/code_graph/graph.json", { head: overlay.head, tree, inputHash: codeInputHash });
5452
+ const memoryGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/graph/graph.json", { head: overlay.head, tree, inputHash: memoryInputHash });
5019
5453
  const errors = [...validation.errors];
5020
5454
  const warnings = [...validation.warnings];
5021
5455
  const requiredActions = [];
@@ -5024,7 +5458,7 @@ function prCheck(projectDir) {
5024
5458
  requiredActions.push("Run kage refresh, then update or supersede stale packets.");
5025
5459
  }
5026
5460
  if (!codeGraphCurrent || !memoryGraphCurrent) {
5027
- errors.push("Generated graph artifacts are missing or not current for this branch head.");
5461
+ errors.push("Generated graph artifacts are missing or not current for this working tree content.");
5028
5462
  requiredActions.push("Run kage refresh --project <dir> before merge.");
5029
5463
  }
5030
5464
  if (!memoryPacketChanges.length && overlay.changed_files.some((path) => !path.startsWith(".agent_memory/"))) {
@@ -5516,9 +5950,9 @@ function installClaudeSettings(projectDir) {
5516
5950
  function initProject(projectDir) {
5517
5951
  installAgentPolicy(projectDir);
5518
5952
  installClaudeSettings(projectDir);
5519
- const index = indexProject(projectDir);
5953
+ const index = indexProject(projectDir, { graphs: false });
5520
5954
  const validation = validateProject(projectDir);
5521
- const sampleRecall = recall(projectDir, "how do I run tests");
5955
+ const sampleRecall = recallFromPackets("how do I run tests", loadApprovedPackets(projectDir), 5, "Repo Memory");
5522
5956
  return { index, validation, sampleRecall };
5523
5957
  }
5524
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.16",
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; }