@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 +28 -4
- package/dist/index.js +1 -1
- package/dist/kernel.js +532 -98
- package/package.json +1 -1
- package/viewer/app.js +92 -1
- package/viewer/index.html +2 -9
- package/viewer/styles.css +49 -0
package/README.md
CHANGED
|
@@ -9,6 +9,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
|
|
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
|
|
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
|
|
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) => [
|
|
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
|
|
1383
|
-
return
|
|
1416
|
+
function emptyCodeIndexManifest(projectDir) {
|
|
1417
|
+
return {
|
|
1418
|
+
schema_version: 1,
|
|
1419
|
+
project_dir: projectDir,
|
|
1420
|
+
repo_key: repoKey(projectDir),
|
|
1421
|
+
generated_at: nowIso(),
|
|
1422
|
+
mode: "quick",
|
|
1423
|
+
limits: {
|
|
1424
|
+
max_file_bytes: MAX_CODE_FILE_BYTES,
|
|
1425
|
+
max_files: MAX_CODE_GRAPH_FILES,
|
|
1426
|
+
max_symbols: MAX_CODE_GRAPH_SYMBOLS,
|
|
1427
|
+
max_calls: MAX_CODE_GRAPH_CALLS,
|
|
1428
|
+
max_calls_per_file: MAX_CODE_GRAPH_CALLS_PER_FILE,
|
|
1429
|
+
},
|
|
1430
|
+
coverage: {
|
|
1431
|
+
indexable_files: 0,
|
|
1432
|
+
indexed_files: 0,
|
|
1433
|
+
deferred_files: 0,
|
|
1434
|
+
ignored_files: 0,
|
|
1435
|
+
coverage_percent: 100,
|
|
1436
|
+
complete: true,
|
|
1437
|
+
},
|
|
1438
|
+
cache: {
|
|
1439
|
+
hits: 0,
|
|
1440
|
+
misses: 0,
|
|
1441
|
+
},
|
|
1442
|
+
deferred_files: [],
|
|
1443
|
+
ignored_summary: {},
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function codeIndexManifestPath(projectDir) {
|
|
1447
|
+
return (0, node_path_1.join)(codeGraphDir(projectDir), "index-manifest.json");
|
|
1448
|
+
}
|
|
1449
|
+
function codeIndexSelection(projectDir) {
|
|
1450
|
+
const candidates = [];
|
|
1451
|
+
const deferred = [];
|
|
1452
|
+
const ignoredSummary = {};
|
|
1453
|
+
const ignore = (reason) => {
|
|
1454
|
+
ignoredSummary[reason] = (ignoredSummary[reason] ?? 0) + 1;
|
|
1455
|
+
};
|
|
1456
|
+
const visit = (dir) => {
|
|
1457
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
1458
|
+
return;
|
|
1459
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir)) {
|
|
1460
|
+
const absolutePath = (0, node_path_1.join)(dir, entry);
|
|
1461
|
+
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1462
|
+
if (shouldSkipCodePath(rel)) {
|
|
1463
|
+
ignore("generated_vendor_or_cache");
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
const stats = (0, node_fs_1.statSync)(absolutePath);
|
|
1467
|
+
if (stats.isDirectory()) {
|
|
1468
|
+
visit(absolutePath);
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
const extension = extensionOf(rel);
|
|
1472
|
+
const indexable = CODE_EXTENSIONS.has(extension) || CONFIG_NAMES.has((0, node_path_1.basename)(rel)) || rel === "README.md";
|
|
1473
|
+
if (!indexable) {
|
|
1474
|
+
ignore("unsupported_file_type");
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (stats.size > MAX_CODE_FILE_BYTES) {
|
|
1478
|
+
deferred.push({ path: rel, size_bytes: stats.size, reason: "over_quick_file_size_limit" });
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
candidates.push(absolutePath);
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
visit(projectDir);
|
|
1485
|
+
const sorted = candidates.sort((a, b) => codeFilePriority(projectDir, a) - codeFilePriority(projectDir, b) || a.localeCompare(b));
|
|
1486
|
+
const indexableFiles = sorted.length + deferred.length;
|
|
1487
|
+
const files = sorted.slice(0, MAX_CODE_GRAPH_FILES);
|
|
1488
|
+
for (const absolutePath of sorted.slice(MAX_CODE_GRAPH_FILES)) {
|
|
1384
1489
|
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
|
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
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
3004
|
+
const built = options.graphs === false ? null : buildGraphIndexes(projectDir);
|
|
3005
|
+
const indexes = built?.indexes ?? buildPacketIndexes(projectDir);
|
|
2640
3006
|
return {
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2785
|
-
|
|
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
|
|
3036
|
-
const
|
|
3037
|
-
const
|
|
3416
|
+
function recallGraphLookup(graph) {
|
|
3417
|
+
const packetEntityByPacketId = new Map();
|
|
3418
|
+
for (const entity of graph.entities) {
|
|
3419
|
+
if (entity.type !== "memory")
|
|
3420
|
+
continue;
|
|
3421
|
+
for (const alias of entity.aliases)
|
|
3422
|
+
packetEntityByPacketId.set(alias, entity.id);
|
|
3423
|
+
}
|
|
3424
|
+
const edgesByEntityId = new Map();
|
|
3425
|
+
for (const edge of graph.edges) {
|
|
3426
|
+
const from = edgesByEntityId.get(edge.from) ?? [];
|
|
3427
|
+
from.push(edge);
|
|
3428
|
+
edgesByEntityId.set(edge.from, from);
|
|
3429
|
+
const to = edgesByEntityId.get(edge.to) ?? [];
|
|
3430
|
+
to.push(edge);
|
|
3431
|
+
edgesByEntityId.set(edge.to, to);
|
|
3432
|
+
}
|
|
3433
|
+
return { packetEntityByPacketId, edgesByEntityId };
|
|
3434
|
+
}
|
|
3435
|
+
function recallBreakdown(projectDir, terms, packet, textScore, graph = buildKnowledgeGraph(projectDir), lookup = recallGraphLookup(graph)) {
|
|
3436
|
+
const packetEntityId = lookup.packetEntityByPacketId.get(packet.id);
|
|
3038
3437
|
const rawGraphScore = packetEntityId
|
|
3039
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3291
|
-
const
|
|
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
|
|
3383
|
-
const
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
package/viewer/app.js
CHANGED
|
@@ -112,6 +112,7 @@
|
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
var MEMORY_CODE_RELATIONS = new Set(["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test", "affects_path"]);
|
|
115
|
+
var INSPECTOR_CONNECTION_LIMIT = 8;
|
|
115
116
|
|
|
116
117
|
els.graphFile.addEventListener("change", handleFile);
|
|
117
118
|
els.searchInput.addEventListener("input", scheduleRender);
|
|
@@ -1373,6 +1374,97 @@
|
|
|
1373
1374
|
els.selectionDetails.appendChild(title);
|
|
1374
1375
|
els.selectionDetails.appendChild(kind);
|
|
1375
1376
|
els.selectionDetails.appendChild(rows);
|
|
1377
|
+
renderInspectorConnections(item);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function renderInspectorConnections(item) {
|
|
1381
|
+
if (state.selected.kind !== "entity") {
|
|
1382
|
+
if (item && isMemoryCodeEdge(item)) {
|
|
1383
|
+
els.selectionDetails.appendChild(detailSection("Memory-Code Link", [
|
|
1384
|
+
connectionText(item, state.entityById.get(item.from), state.entityById.get(item.to))
|
|
1385
|
+
], 0));
|
|
1386
|
+
}
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
var links = memoryCodeConnections(item.id);
|
|
1391
|
+
if (!links.length) return;
|
|
1392
|
+
var rows = links.slice(0, INSPECTOR_CONNECTION_LIMIT).map(function (link) {
|
|
1393
|
+
return connectionText(link.edge, item, link.other);
|
|
1394
|
+
});
|
|
1395
|
+
els.selectionDetails.appendChild(detailSection("Memory-Code Evidence", rows, links.length - rows.length));
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function memoryCodeConnections(entityId) {
|
|
1399
|
+
return state.edges
|
|
1400
|
+
.filter(function (edge) { return isMemoryCodeEdge(edge) && (edge.from === entityId || edge.to === entityId); })
|
|
1401
|
+
.map(function (edge) {
|
|
1402
|
+
var otherId = edge.from === entityId ? edge.to : edge.from;
|
|
1403
|
+
return { edge: edge, other: state.entityById.get(otherId) };
|
|
1404
|
+
})
|
|
1405
|
+
.filter(function (link) { return Boolean(link.other); })
|
|
1406
|
+
.sort(function (a, b) {
|
|
1407
|
+
return connectionImportance(b) - connectionImportance(a) ||
|
|
1408
|
+
displayName(a.other).localeCompare(displayName(b.other));
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function isMemoryCodeEdge(edge) {
|
|
1413
|
+
return Boolean(edge && (edge.memory_code_link || isMemoryCodeRelation(edge.relation)));
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function connectionImportance(link) {
|
|
1417
|
+
var relation = String(link.edge.relation || "");
|
|
1418
|
+
var score = entityImportance(link.other);
|
|
1419
|
+
if (["fixes_symbol", "verified_by_test"].indexOf(relation) !== -1) score += 36;
|
|
1420
|
+
if (["explains_symbol", "applies_to_route"].indexOf(relation) !== -1) score += 24;
|
|
1421
|
+
if (link.other.graph_kind === "memory") score += 18;
|
|
1422
|
+
return score;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function connectionText(edge, selected, other) {
|
|
1426
|
+
var from = state.entityById.get(edge.from);
|
|
1427
|
+
var to = state.entityById.get(edge.to);
|
|
1428
|
+
var peer = other || (selected && selected.id === edge.from ? to : from);
|
|
1429
|
+
var label = peer ? displayName(peer) : displayName(from) + " -> " + displayName(to);
|
|
1430
|
+
var meta = [edge.relation || "related", peer && (peer.graph_kind || peer.type)].filter(Boolean).join(" | ");
|
|
1431
|
+
return {
|
|
1432
|
+
label: label,
|
|
1433
|
+
meta: meta,
|
|
1434
|
+
body: edge.fact || "",
|
|
1435
|
+
edge: edge,
|
|
1436
|
+
entity: peer
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function detailSection(title, items, hiddenCount) {
|
|
1441
|
+
var section = document.createElement("section");
|
|
1442
|
+
section.className = "detail-section";
|
|
1443
|
+
var heading = document.createElement("div");
|
|
1444
|
+
heading.className = "detail-section-title";
|
|
1445
|
+
heading.textContent = title;
|
|
1446
|
+
section.appendChild(heading);
|
|
1447
|
+
items.forEach(function (item) {
|
|
1448
|
+
var button = document.createElement("button");
|
|
1449
|
+
button.type = "button";
|
|
1450
|
+
button.className = "detail-link";
|
|
1451
|
+
button.innerHTML = "<span class=\"detail-link-title\"></span><span class=\"detail-link-meta\"></span><span class=\"detail-link-body\"></span>";
|
|
1452
|
+
button.querySelector(".detail-link-title").textContent = item.label;
|
|
1453
|
+
button.querySelector(".detail-link-meta").textContent = item.meta;
|
|
1454
|
+
button.querySelector(".detail-link-body").textContent = item.body;
|
|
1455
|
+
button.addEventListener("click", function () {
|
|
1456
|
+
state.selected = item.entity ? { kind: "entity", id: item.entity.id } : { kind: "edge", id: item.edge.id };
|
|
1457
|
+
render();
|
|
1458
|
+
});
|
|
1459
|
+
section.appendChild(button);
|
|
1460
|
+
});
|
|
1461
|
+
if (hiddenCount > 0) {
|
|
1462
|
+
var more = document.createElement("div");
|
|
1463
|
+
more.className = "detail-more";
|
|
1464
|
+
more.textContent = "+" + hiddenCount + " more connected items hidden to keep the graph readable.";
|
|
1465
|
+
section.appendChild(more);
|
|
1466
|
+
}
|
|
1467
|
+
return section;
|
|
1376
1468
|
}
|
|
1377
1469
|
|
|
1378
1470
|
function detailRow(label, value) {
|
|
@@ -1727,7 +1819,6 @@
|
|
|
1727
1819
|
var node = findCanvasNode(world.x, world.y);
|
|
1728
1820
|
if (!node) return;
|
|
1729
1821
|
state.selected = { kind: "entity", id: node.id };
|
|
1730
|
-
els.scopeFilter.value = "focus";
|
|
1731
1822
|
render();
|
|
1732
1823
|
}
|
|
1733
1824
|
|
package/viewer/index.html
CHANGED
|
@@ -86,21 +86,14 @@
|
|
|
86
86
|
<option value="">All relations</option>
|
|
87
87
|
</select>
|
|
88
88
|
</label>
|
|
89
|
-
<
|
|
90
|
-
<span>Scope</span>
|
|
91
|
-
<select id="scopeFilter">
|
|
92
|
-
<option value="signal">High signal</option>
|
|
93
|
-
<option value="focus">Focus selection</option>
|
|
94
|
-
<option value="all">Everything</option>
|
|
95
|
-
</select>
|
|
96
|
-
</label>
|
|
89
|
+
<input id="scopeFilter" type="hidden" value="signal">
|
|
97
90
|
<label>
|
|
98
91
|
<span>Max Nodes</span>
|
|
99
92
|
<select id="maxNodes">
|
|
100
93
|
<option value="60">60</option>
|
|
101
94
|
<option value="90" selected>90</option>
|
|
102
95
|
<option value="140">140</option>
|
|
103
|
-
<option value="
|
|
96
|
+
<option value="220">220</option>
|
|
104
97
|
</select>
|
|
105
98
|
</label>
|
|
106
99
|
<label class="toggle-control">
|
package/viewer/styles.css
CHANGED
|
@@ -435,6 +435,55 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
435
435
|
.detail-row { padding: 9px 0; border-top: 1px solid var(--line); }
|
|
436
436
|
.detail-row dt { margin: 0 0 4px; color: var(--terminal-dim); font-size: 11px; font-weight: 760; text-transform: uppercase; }
|
|
437
437
|
.detail-row dd { margin: 0; color: var(--text); overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
438
|
+
.detail-section {
|
|
439
|
+
margin-top: 12px;
|
|
440
|
+
padding-top: 12px;
|
|
441
|
+
border-top: 1px solid var(--line);
|
|
442
|
+
}
|
|
443
|
+
.detail-section-title {
|
|
444
|
+
margin-bottom: 8px;
|
|
445
|
+
color: var(--terminal-dim);
|
|
446
|
+
font-size: 11px;
|
|
447
|
+
font-weight: 800;
|
|
448
|
+
text-transform: uppercase;
|
|
449
|
+
}
|
|
450
|
+
.detail-link {
|
|
451
|
+
width: 100%;
|
|
452
|
+
display: grid;
|
|
453
|
+
gap: 4px;
|
|
454
|
+
min-height: 0;
|
|
455
|
+
margin-top: 8px;
|
|
456
|
+
padding: 9px;
|
|
457
|
+
border-color: rgba(216, 255, 95, 0.28);
|
|
458
|
+
background: rgba(216, 255, 95, 0.045);
|
|
459
|
+
color: var(--text);
|
|
460
|
+
text-align: left;
|
|
461
|
+
white-space: normal;
|
|
462
|
+
box-shadow: none;
|
|
463
|
+
}
|
|
464
|
+
.detail-link:hover { background: rgba(216, 255, 95, 0.085); }
|
|
465
|
+
.detail-link-title {
|
|
466
|
+
color: var(--terminal);
|
|
467
|
+
font-weight: 820;
|
|
468
|
+
overflow-wrap: anywhere;
|
|
469
|
+
}
|
|
470
|
+
.detail-link-meta {
|
|
471
|
+
color: var(--warn);
|
|
472
|
+
font-size: 10px;
|
|
473
|
+
font-weight: 760;
|
|
474
|
+
text-transform: uppercase;
|
|
475
|
+
overflow-wrap: anywhere;
|
|
476
|
+
}
|
|
477
|
+
.detail-link-body {
|
|
478
|
+
color: var(--muted);
|
|
479
|
+
font-size: 11px;
|
|
480
|
+
overflow-wrap: anywhere;
|
|
481
|
+
}
|
|
482
|
+
.detail-more {
|
|
483
|
+
margin-top: 9px;
|
|
484
|
+
color: var(--terminal-dim);
|
|
485
|
+
font-size: 11px;
|
|
486
|
+
}
|
|
438
487
|
|
|
439
488
|
.entities-panel { grid-area: entities; }
|
|
440
489
|
.edges-panel { grid-area: edges; }
|