@kage-core/kage-graph-mcp 1.1.17 → 1.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/kernel.js +499 -88
- package/package.json +1 -1
- package/viewer/app.js +92 -1
- package/viewer/index.html +2 -9
- package/viewer/styles.css +49 -0
package/README.md
CHANGED
|
@@ -9,6 +9,19 @@ This package exposes two surfaces:
|
|
|
9
9
|
|
|
10
10
|
## Latest Release
|
|
11
11
|
|
|
12
|
+
`1.1.18` publishes the end-to-end performance pass:
|
|
13
|
+
|
|
14
|
+
- read-only commands reuse current graph artifacts instead of rebuilding them
|
|
15
|
+
when inputs are fresh.
|
|
16
|
+
- MCP sessions keep an in-process graph cache, so repeated agent calls do not
|
|
17
|
+
keep reparsing the same graph JSON.
|
|
18
|
+
- `kage refresh` reports lightweight freshness metrics and leaves deep
|
|
19
|
+
benchmark/quality work to explicit `kage metrics` and `kage benchmark` calls.
|
|
20
|
+
- recall builds graph lookup maps once per query instead of scanning all graph
|
|
21
|
+
entities and edges for every memory packet.
|
|
22
|
+
- `kage init` remains a packet-only bootstrap path; full graph generation stays
|
|
23
|
+
with `kage refresh` and `kage index`.
|
|
24
|
+
|
|
12
25
|
`1.1.17` publishes content-based graph freshness:
|
|
13
26
|
|
|
14
27
|
- `kage pr check` now uses graph input hashes, so push-only operations and
|
package/dist/kernel.js
CHANGED
|
@@ -134,6 +134,7 @@ exports.MEMORY_TYPES = [
|
|
|
134
134
|
"negative_result",
|
|
135
135
|
"constraint",
|
|
136
136
|
];
|
|
137
|
+
const graphMemoryCache = new Map();
|
|
137
138
|
exports.SETUP_AGENTS = [
|
|
138
139
|
"codex",
|
|
139
140
|
"claude-code",
|
|
@@ -1297,6 +1298,11 @@ const CODE_EXTENSIONS = new Set([
|
|
|
1297
1298
|
".hpp",
|
|
1298
1299
|
".swift",
|
|
1299
1300
|
]);
|
|
1301
|
+
const MAX_CODE_FILE_BYTES = positiveIntEnv("KAGE_MAX_CODE_FILE_BYTES", 512 * 1024);
|
|
1302
|
+
const MAX_CODE_GRAPH_FILES = positiveIntEnv("KAGE_MAX_CODE_GRAPH_FILES", 2000);
|
|
1303
|
+
const MAX_CODE_GRAPH_SYMBOLS = positiveIntEnv("KAGE_MAX_CODE_GRAPH_SYMBOLS", 25000);
|
|
1304
|
+
const MAX_CODE_GRAPH_CALLS = positiveIntEnv("KAGE_MAX_CODE_GRAPH_CALLS", 50000);
|
|
1305
|
+
const MAX_CODE_GRAPH_CALLS_PER_FILE = positiveIntEnv("KAGE_MAX_CODE_GRAPH_CALLS_PER_FILE", 250);
|
|
1300
1306
|
const CONFIG_NAMES = new Set([
|
|
1301
1307
|
"package.json",
|
|
1302
1308
|
"pyproject.toml",
|
|
@@ -1315,6 +1321,10 @@ const CONFIG_NAMES = new Set([
|
|
|
1315
1321
|
"vitest.config.js",
|
|
1316
1322
|
"vitest.config.ts",
|
|
1317
1323
|
]);
|
|
1324
|
+
function positiveIntEnv(name, fallback) {
|
|
1325
|
+
const value = Number(process.env[name]);
|
|
1326
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
1327
|
+
}
|
|
1318
1328
|
function extensionOf(path) {
|
|
1319
1329
|
const match = path.match(/\.[^.\/]+$/);
|
|
1320
1330
|
return match ? match[0] : "";
|
|
@@ -1322,7 +1332,26 @@ function extensionOf(path) {
|
|
|
1322
1332
|
function shouldSkipCodePath(relativePath) {
|
|
1323
1333
|
return relativePath
|
|
1324
1334
|
.split("/")
|
|
1325
|
-
.some((part) => [
|
|
1335
|
+
.some((part) => [
|
|
1336
|
+
".git",
|
|
1337
|
+
".agent_memory",
|
|
1338
|
+
"node_modules",
|
|
1339
|
+
"vendor",
|
|
1340
|
+
".venv",
|
|
1341
|
+
"venv",
|
|
1342
|
+
"__pycache__",
|
|
1343
|
+
"dist",
|
|
1344
|
+
"build",
|
|
1345
|
+
"coverage",
|
|
1346
|
+
".next",
|
|
1347
|
+
".nuxt",
|
|
1348
|
+
".output",
|
|
1349
|
+
".turbo",
|
|
1350
|
+
".cache",
|
|
1351
|
+
".parcel-cache",
|
|
1352
|
+
"target",
|
|
1353
|
+
".gradle",
|
|
1354
|
+
].includes(part));
|
|
1326
1355
|
}
|
|
1327
1356
|
function codeLanguage(path) {
|
|
1328
1357
|
const extension = extensionOf(path);
|
|
@@ -1384,14 +1413,214 @@ function codeFileKind(path) {
|
|
|
1384
1413
|
return "doc";
|
|
1385
1414
|
return "source";
|
|
1386
1415
|
}
|
|
1387
|
-
function
|
|
1388
|
-
return
|
|
1416
|
+
function emptyCodeIndexManifest(projectDir) {
|
|
1417
|
+
return {
|
|
1418
|
+
schema_version: 1,
|
|
1419
|
+
project_dir: projectDir,
|
|
1420
|
+
repo_key: repoKey(projectDir),
|
|
1421
|
+
generated_at: nowIso(),
|
|
1422
|
+
mode: "quick",
|
|
1423
|
+
limits: {
|
|
1424
|
+
max_file_bytes: MAX_CODE_FILE_BYTES,
|
|
1425
|
+
max_files: MAX_CODE_GRAPH_FILES,
|
|
1426
|
+
max_symbols: MAX_CODE_GRAPH_SYMBOLS,
|
|
1427
|
+
max_calls: MAX_CODE_GRAPH_CALLS,
|
|
1428
|
+
max_calls_per_file: MAX_CODE_GRAPH_CALLS_PER_FILE,
|
|
1429
|
+
},
|
|
1430
|
+
coverage: {
|
|
1431
|
+
indexable_files: 0,
|
|
1432
|
+
indexed_files: 0,
|
|
1433
|
+
deferred_files: 0,
|
|
1434
|
+
ignored_files: 0,
|
|
1435
|
+
coverage_percent: 100,
|
|
1436
|
+
complete: true,
|
|
1437
|
+
},
|
|
1438
|
+
cache: {
|
|
1439
|
+
hits: 0,
|
|
1440
|
+
misses: 0,
|
|
1441
|
+
},
|
|
1442
|
+
deferred_files: [],
|
|
1443
|
+
ignored_summary: {},
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function codeIndexManifestPath(projectDir) {
|
|
1447
|
+
return (0, node_path_1.join)(codeGraphDir(projectDir), "index-manifest.json");
|
|
1448
|
+
}
|
|
1449
|
+
function codeIndexSelection(projectDir) {
|
|
1450
|
+
const candidates = [];
|
|
1451
|
+
const deferred = [];
|
|
1452
|
+
const ignoredSummary = {};
|
|
1453
|
+
const ignore = (reason) => {
|
|
1454
|
+
ignoredSummary[reason] = (ignoredSummary[reason] ?? 0) + 1;
|
|
1455
|
+
};
|
|
1456
|
+
const visit = (dir) => {
|
|
1457
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
1458
|
+
return;
|
|
1459
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir)) {
|
|
1460
|
+
const absolutePath = (0, node_path_1.join)(dir, entry);
|
|
1461
|
+
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1462
|
+
if (shouldSkipCodePath(rel)) {
|
|
1463
|
+
ignore("generated_vendor_or_cache");
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
const stats = (0, node_fs_1.statSync)(absolutePath);
|
|
1467
|
+
if (stats.isDirectory()) {
|
|
1468
|
+
visit(absolutePath);
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
const extension = extensionOf(rel);
|
|
1472
|
+
const indexable = CODE_EXTENSIONS.has(extension) || CONFIG_NAMES.has((0, node_path_1.basename)(rel)) || rel === "README.md";
|
|
1473
|
+
if (!indexable) {
|
|
1474
|
+
ignore("unsupported_file_type");
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (stats.size > MAX_CODE_FILE_BYTES) {
|
|
1478
|
+
deferred.push({ path: rel, size_bytes: stats.size, reason: "over_quick_file_size_limit" });
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
candidates.push(absolutePath);
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
visit(projectDir);
|
|
1485
|
+
const sorted = candidates.sort((a, b) => codeFilePriority(projectDir, a) - codeFilePriority(projectDir, b) || a.localeCompare(b));
|
|
1486
|
+
const indexableFiles = sorted.length + deferred.length;
|
|
1487
|
+
const files = sorted.slice(0, MAX_CODE_GRAPH_FILES);
|
|
1488
|
+
for (const absolutePath of sorted.slice(MAX_CODE_GRAPH_FILES)) {
|
|
1389
1489
|
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1490
|
+
deferred.push({ path: rel, size_bytes: (0, node_fs_1.statSync)(absolutePath).size, reason: "over_quick_file_count_limit" });
|
|
1491
|
+
}
|
|
1492
|
+
const manifest = emptyCodeIndexManifest(projectDir);
|
|
1493
|
+
manifest.coverage = {
|
|
1494
|
+
indexable_files: indexableFiles,
|
|
1495
|
+
indexed_files: files.length,
|
|
1496
|
+
deferred_files: deferred.length,
|
|
1497
|
+
ignored_files: Object.values(ignoredSummary).reduce((sum, count) => sum + count, 0),
|
|
1498
|
+
coverage_percent: percent(files.length, indexableFiles),
|
|
1499
|
+
complete: deferred.length === 0,
|
|
1500
|
+
};
|
|
1501
|
+
manifest.deferred_files = deferred.sort((a, b) => a.path.localeCompare(b.path));
|
|
1502
|
+
manifest.ignored_summary = Object.fromEntries(Object.entries(ignoredSummary).sort(([a], [b]) => a.localeCompare(b)));
|
|
1503
|
+
return { files, manifest };
|
|
1504
|
+
}
|
|
1505
|
+
function writeCodeIndexManifest(projectDir, manifest) {
|
|
1506
|
+
writeJson(codeIndexManifestPath(projectDir), manifest);
|
|
1507
|
+
}
|
|
1508
|
+
function readCodeIndexManifest(projectDir) {
|
|
1509
|
+
const path = codeIndexManifestPath(projectDir);
|
|
1510
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
1511
|
+
return emptyCodeIndexManifest(projectDir);
|
|
1512
|
+
try {
|
|
1513
|
+
const manifest = readJson(path);
|
|
1514
|
+
if (!manifest.cache)
|
|
1515
|
+
manifest.cache = { hits: 0, misses: 0 };
|
|
1516
|
+
return manifest;
|
|
1517
|
+
}
|
|
1518
|
+
catch {
|
|
1519
|
+
return emptyCodeIndexManifest(projectDir);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function listCodeFiles(projectDir) {
|
|
1523
|
+
return codeIndexSelection(projectDir).files;
|
|
1524
|
+
}
|
|
1525
|
+
function codeGraphStatFingerprint(projectDir, absoluteFiles) {
|
|
1526
|
+
const entries = [
|
|
1527
|
+
...absoluteFiles,
|
|
1528
|
+
...externalIndexFiles(projectDir).map((index) => index.path),
|
|
1529
|
+
...["package.json", "requirements.txt", "go.mod", "Cargo.toml"]
|
|
1530
|
+
.map((path) => (0, node_path_1.join)(projectDir, path))
|
|
1531
|
+
.filter((path) => (0, node_fs_1.existsSync)(path)),
|
|
1532
|
+
]
|
|
1533
|
+
.filter((path) => (0, node_fs_1.existsSync)(path))
|
|
1534
|
+
.map((path) => {
|
|
1535
|
+
const stats = (0, node_fs_1.statSync)(path);
|
|
1536
|
+
return `${projectRelative(projectDir, path)}:${stats.size}:${Math.round(stats.mtimeMs)}`;
|
|
1537
|
+
})
|
|
1538
|
+
.sort();
|
|
1539
|
+
return sha256Hex(entries.join("\n"));
|
|
1540
|
+
}
|
|
1541
|
+
function readCachedCodeGraph(projectDir, fingerprint) {
|
|
1542
|
+
const path = (0, node_path_1.join)(codeGraphDir(projectDir), "graph.json");
|
|
1543
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
1544
|
+
return null;
|
|
1545
|
+
try {
|
|
1546
|
+
const graph = readJson(path);
|
|
1547
|
+
if (readCodeIndexManifest(projectDir).fingerprint !== fingerprint)
|
|
1548
|
+
return null;
|
|
1549
|
+
return graph;
|
|
1550
|
+
}
|
|
1551
|
+
catch {
|
|
1552
|
+
return null;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
function fileFactCacheDir(projectDir) {
|
|
1556
|
+
return (0, node_path_1.join)(codeGraphDir(projectDir), "file-cache");
|
|
1557
|
+
}
|
|
1558
|
+
function fileFactCachePath(projectDir, rel, hash) {
|
|
1559
|
+
return (0, node_path_1.join)(fileFactCacheDir(projectDir), `${slugify(rel)}-${hash}.json`);
|
|
1560
|
+
}
|
|
1561
|
+
function readCachedFileFacts(projectDir, rel, hash) {
|
|
1562
|
+
const path = fileFactCachePath(projectDir, rel, hash);
|
|
1563
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
1564
|
+
return null;
|
|
1565
|
+
try {
|
|
1566
|
+
const cached = readJson(path);
|
|
1567
|
+
if (cached.schema_version !== 1 || cached.path !== rel || cached.hash !== hash)
|
|
1568
|
+
return null;
|
|
1569
|
+
if (!cached.file || !Array.isArray(cached.symbols) || !Array.isArray(cached.imports))
|
|
1570
|
+
return null;
|
|
1571
|
+
return cached;
|
|
1572
|
+
}
|
|
1573
|
+
catch {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function writeCachedFileFacts(projectDir, facts) {
|
|
1578
|
+
ensureDir(fileFactCacheDir(projectDir));
|
|
1579
|
+
writeJson(fileFactCachePath(projectDir, facts.path, facts.hash), facts);
|
|
1580
|
+
}
|
|
1581
|
+
function buildFileFacts(projectDir, absolutePath, knownFiles) {
|
|
1582
|
+
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1583
|
+
const content = (0, node_fs_1.readFileSync)(absolutePath, "utf8");
|
|
1584
|
+
const fullHash = (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
|
|
1585
|
+
const cached = readCachedFileFacts(projectDir, rel, fullHash);
|
|
1586
|
+
if (cached)
|
|
1587
|
+
return { facts: cached, content, cacheHit: true };
|
|
1588
|
+
const file = {
|
|
1589
|
+
id: `file:${slugify(rel)}`,
|
|
1590
|
+
path: rel,
|
|
1591
|
+
language: codeLanguage(rel),
|
|
1592
|
+
parser: codeParser(rel),
|
|
1593
|
+
kind: codeFileKind(rel),
|
|
1594
|
+
size_bytes: Buffer.byteLength(content),
|
|
1595
|
+
line_count: content.split(/\r?\n/).length,
|
|
1596
|
+
hash: fullHash.slice(0, 16),
|
|
1597
|
+
};
|
|
1598
|
+
const symbols = [];
|
|
1599
|
+
const imports = [];
|
|
1600
|
+
if (TS_AST_EXTENSIONS.has(extensionOf(rel))) {
|
|
1601
|
+
symbols.push(...extractSymbols(rel, content));
|
|
1602
|
+
imports.push(...extractImports(projectDir, rel, content, knownFiles));
|
|
1603
|
+
}
|
|
1604
|
+
else if (CODE_EXTENSIONS.has(extensionOf(rel))) {
|
|
1605
|
+
symbols.push(...extractGenericSymbols(rel, content));
|
|
1606
|
+
imports.push(...extractGenericImports(projectDir, rel, content, knownFiles));
|
|
1607
|
+
}
|
|
1608
|
+
const facts = { schema_version: 1, path: rel, hash: fullHash, file, symbols, imports };
|
|
1609
|
+
writeCachedFileFacts(projectDir, facts);
|
|
1610
|
+
return { facts, content, cacheHit: false };
|
|
1611
|
+
}
|
|
1612
|
+
function codeFilePriority(projectDir, absolutePath) {
|
|
1613
|
+
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1614
|
+
const kind = codeFileKind(rel);
|
|
1615
|
+
if (rel === "README.md" || CONFIG_NAMES.has((0, node_path_1.basename)(rel)))
|
|
1616
|
+
return 0;
|
|
1617
|
+
if (kind === "manifest" || kind === "config")
|
|
1618
|
+
return 1;
|
|
1619
|
+
if (kind === "test")
|
|
1620
|
+
return 2;
|
|
1621
|
+
if (TS_AST_EXTENSIONS.has(extensionOf(rel)))
|
|
1622
|
+
return 3;
|
|
1623
|
+
return 4;
|
|
1395
1624
|
}
|
|
1396
1625
|
function lineForOffset(text, offset) {
|
|
1397
1626
|
return text.slice(0, offset).split(/\r?\n/).length;
|
|
@@ -1779,6 +2008,8 @@ function extractCalls(path, text, symbols, symbolByName) {
|
|
|
1779
2008
|
const sourceFile = sourceFileFor(path, text);
|
|
1780
2009
|
const calls = [];
|
|
1781
2010
|
const visit = (node) => {
|
|
2011
|
+
if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
|
|
2012
|
+
return;
|
|
1782
2013
|
if (!ts.isCallExpression(node)) {
|
|
1783
2014
|
ts.forEachChild(node, visit);
|
|
1784
2015
|
return;
|
|
@@ -1796,6 +2027,8 @@ function extractCalls(path, text, symbols, symbolByName) {
|
|
|
1796
2027
|
const line = lineForNode(sourceFile, node);
|
|
1797
2028
|
const caller = symbolAtLine(symbols, path, line);
|
|
1798
2029
|
for (const target of targets.slice(0, 3)) {
|
|
2030
|
+
if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
|
|
2031
|
+
break;
|
|
1799
2032
|
if (target.path === path && target.line === line)
|
|
1800
2033
|
continue;
|
|
1801
2034
|
calls.push({ from_symbol: caller?.id ?? null, to_symbol: target.id, path, line });
|
|
@@ -1896,9 +2129,9 @@ function fileInputEntries(projectDir, paths, kind) {
|
|
|
1896
2129
|
sha256: sha256Hex((0, node_fs_1.readFileSync)(path)),
|
|
1897
2130
|
}));
|
|
1898
2131
|
}
|
|
1899
|
-
function codeGraphInputHash(projectDir) {
|
|
2132
|
+
function codeGraphInputHash(projectDir, absoluteFiles = listCodeFiles(projectDir)) {
|
|
1900
2133
|
return graphInputHash([
|
|
1901
|
-
...fileInputEntries(projectDir,
|
|
2134
|
+
...fileInputEntries(projectDir, absoluteFiles, "code_file"),
|
|
1902
2135
|
...fileInputEntries(projectDir, externalIndexFiles(projectDir).map((index) => index.path), "external_code_index"),
|
|
1903
2136
|
]);
|
|
1904
2137
|
}
|
|
@@ -2153,39 +2386,44 @@ function buildCodeGraph(projectDir) {
|
|
|
2153
2386
|
const head = gitHead(projectDir);
|
|
2154
2387
|
const tree = gitTree(projectDir);
|
|
2155
2388
|
const mergeBase = gitMergeBase(projectDir);
|
|
2156
|
-
const
|
|
2157
|
-
const absoluteFiles =
|
|
2389
|
+
const selection = codeIndexSelection(projectDir);
|
|
2390
|
+
const absoluteFiles = selection.files;
|
|
2391
|
+
const fingerprint = codeGraphStatFingerprint(projectDir, absoluteFiles);
|
|
2392
|
+
const cachedGraph = readCachedCodeGraph(projectDir, fingerprint);
|
|
2393
|
+
if (cachedGraph) {
|
|
2394
|
+
selection.manifest.cache = { hits: absoluteFiles.length, misses: 0 };
|
|
2395
|
+
selection.manifest.fingerprint = fingerprint;
|
|
2396
|
+
writeCodeIndexManifest(projectDir, selection.manifest);
|
|
2397
|
+
return cachedGraph;
|
|
2398
|
+
}
|
|
2399
|
+
const inputHash = codeGraphInputHash(projectDir, absoluteFiles);
|
|
2400
|
+
selection.manifest.fingerprint = fingerprint;
|
|
2401
|
+
writeCodeIndexManifest(projectDir, selection.manifest);
|
|
2158
2402
|
const knownFiles = new Set(absoluteFiles.map((path) => (0, node_path_1.relative)(projectDir, path).replace(/\\/g, "/")));
|
|
2159
2403
|
const files = [];
|
|
2160
2404
|
const symbols = [];
|
|
2161
2405
|
const imports = [];
|
|
2162
2406
|
const contents = new Map();
|
|
2407
|
+
let cacheHits = 0;
|
|
2408
|
+
let cacheMisses = 0;
|
|
2163
2409
|
for (const absolutePath of absoluteFiles) {
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
});
|
|
2177
|
-
if (TS_AST_EXTENSIONS.has(extensionOf(rel))) {
|
|
2178
|
-
symbols.push(...extractSymbols(rel, content));
|
|
2179
|
-
imports.push(...extractImports(projectDir, rel, content, knownFiles));
|
|
2180
|
-
}
|
|
2181
|
-
else if (CODE_EXTENSIONS.has(extensionOf(rel))) {
|
|
2182
|
-
symbols.push(...extractGenericSymbols(rel, content));
|
|
2183
|
-
imports.push(...extractGenericImports(projectDir, rel, content, knownFiles));
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2410
|
+
const { facts, content, cacheHit } = buildFileFacts(projectDir, absolutePath, knownFiles);
|
|
2411
|
+
if (cacheHit)
|
|
2412
|
+
cacheHits++;
|
|
2413
|
+
else
|
|
2414
|
+
cacheMisses++;
|
|
2415
|
+
contents.set(facts.path, content);
|
|
2416
|
+
files.push(facts.file);
|
|
2417
|
+
symbols.push(...facts.symbols.slice(0, Math.max(0, MAX_CODE_GRAPH_SYMBOLS - symbols.length)));
|
|
2418
|
+
imports.push(...facts.imports);
|
|
2419
|
+
}
|
|
2420
|
+
selection.manifest.cache = { hits: cacheHits, misses: cacheMisses };
|
|
2421
|
+
writeCodeIndexManifest(projectDir, selection.manifest);
|
|
2186
2422
|
const externalFacts = loadExternalCodeFacts(projectDir);
|
|
2187
2423
|
const fileByPath = new Map(files.map((file) => [file.path, file]));
|
|
2188
2424
|
const addSymbol = (symbol) => {
|
|
2425
|
+
if (symbols.length >= MAX_CODE_GRAPH_SYMBOLS)
|
|
2426
|
+
return;
|
|
2189
2427
|
if (!fileByPath.has(symbol.path))
|
|
2190
2428
|
return;
|
|
2191
2429
|
const file = fileByPath.get(symbol.path);
|
|
@@ -2223,13 +2461,17 @@ function buildCodeGraph(projectDir) {
|
|
|
2223
2461
|
for (const [rel, content] of contents) {
|
|
2224
2462
|
if (!TS_AST_EXTENSIONS.has(extensionOf(rel)))
|
|
2225
2463
|
continue;
|
|
2464
|
+
if (calls.length >= MAX_CODE_GRAPH_CALLS)
|
|
2465
|
+
break;
|
|
2226
2466
|
const fileSymbols = symbols.filter((symbol) => symbol.path === rel);
|
|
2227
2467
|
const fileImports = imports.filter((item) => item.from_path === rel);
|
|
2228
|
-
calls.push(...extractCalls(rel, content,
|
|
2468
|
+
calls.push(...extractCalls(rel, content, fileSymbols, symbolByName).slice(0, Math.max(0, MAX_CODE_GRAPH_CALLS - calls.length)));
|
|
2229
2469
|
routes.push(...extractRoutes(rel, content, fileSymbols));
|
|
2230
2470
|
tests.push(...extractTests(rel, content, fileSymbols, fileImports));
|
|
2231
2471
|
}
|
|
2232
2472
|
for (const call of externalFacts.calls) {
|
|
2473
|
+
if (calls.length >= MAX_CODE_GRAPH_CALLS)
|
|
2474
|
+
break;
|
|
2233
2475
|
if (!calls.some((existing) => existing.from_symbol === call.from_symbol && existing.to_symbol === call.to_symbol && existing.path === call.path && existing.line === call.line))
|
|
2234
2476
|
calls.push(call);
|
|
2235
2477
|
}
|
|
@@ -2255,9 +2497,10 @@ function buildCodeGraph(projectDir) {
|
|
|
2255
2497
|
writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "tests.json"), graph.tests);
|
|
2256
2498
|
writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "packages.json"), graph.packages);
|
|
2257
2499
|
writeJson((0, node_path_1.join)(codeGraphDir(projectDir), "graph.json"), graph);
|
|
2500
|
+
graphMemoryCache.delete((0, node_path_1.resolve)(projectDir));
|
|
2258
2501
|
return graph;
|
|
2259
2502
|
}
|
|
2260
|
-
function buildKnowledgeGraph(projectDir) {
|
|
2503
|
+
function buildKnowledgeGraph(projectDir, codeGraph = buildCodeGraph(projectDir)) {
|
|
2261
2504
|
ensureMemoryDirs(projectDir);
|
|
2262
2505
|
const packets = loadApprovedPackets(projectDir).sort((a, b) => a.id.localeCompare(b.id));
|
|
2263
2506
|
const branch = gitBranch(projectDir);
|
|
@@ -2269,7 +2512,6 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2269
2512
|
const episodes = [];
|
|
2270
2513
|
const repoEntityId = graphEntityId("repo", repoKey(projectDir));
|
|
2271
2514
|
const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
|
|
2272
|
-
const codeGraph = buildCodeGraph(projectDir);
|
|
2273
2515
|
const inputHash = knowledgeGraphInputHash(projectDir, codeGraph.repo_state.input_hash ?? codeGraphInputHash(projectDir));
|
|
2274
2516
|
addEntity(entities, {
|
|
2275
2517
|
id: repoEntityId,
|
|
@@ -2601,13 +2843,12 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2601
2843
|
writeJson((0, node_path_1.join)(graphDir(projectDir), "entities.json"), graph.entities);
|
|
2602
2844
|
writeJson((0, node_path_1.join)(graphDir(projectDir), "edges.json"), graph.edges);
|
|
2603
2845
|
writeJson((0, node_path_1.join)(graphDir(projectDir), "graph.json"), graph);
|
|
2846
|
+
graphMemoryCache.delete((0, node_path_1.resolve)(projectDir));
|
|
2604
2847
|
return graph;
|
|
2605
2848
|
}
|
|
2606
|
-
function
|
|
2849
|
+
function buildPacketIndexes(projectDir) {
|
|
2607
2850
|
ensureMemoryDirs(projectDir);
|
|
2608
2851
|
const packets = loadPacketsFromDir(packetsDir(projectDir)).sort((a, b) => a.id.localeCompare(b.id));
|
|
2609
|
-
const knowledgeGraph = buildKnowledgeGraph(projectDir);
|
|
2610
|
-
const codeGraph = buildCodeGraph(projectDir);
|
|
2611
2852
|
const byPath = {};
|
|
2612
2853
|
const byTag = {};
|
|
2613
2854
|
const byType = {};
|
|
@@ -2644,14 +2885,111 @@ function buildIndexes(projectDir) {
|
|
|
2644
2885
|
(0, node_path_1.join)(indexesDir(projectDir), "by-path.json"),
|
|
2645
2886
|
(0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
|
|
2646
2887
|
(0, node_path_1.join)(indexesDir(projectDir), "by-type.json"),
|
|
2647
|
-
(0, node_path_1.join)(indexesDir(projectDir), "graph.json"),
|
|
2648
|
-
(0, node_path_1.join)(indexesDir(projectDir), "code-graph.json"),
|
|
2649
2888
|
];
|
|
2650
2889
|
writeJson(written[0], catalog);
|
|
2651
2890
|
writeJson(written[1], byPath);
|
|
2652
2891
|
writeJson(written[2], byTag);
|
|
2653
2892
|
writeJson(written[3], byType);
|
|
2654
|
-
|
|
2893
|
+
return written;
|
|
2894
|
+
}
|
|
2895
|
+
function readCurrentCodeGraph(projectDir, expectedInputHash) {
|
|
2896
|
+
const path = (0, node_path_1.join)(codeGraphDir(projectDir), "graph.json");
|
|
2897
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
2898
|
+
return null;
|
|
2899
|
+
try {
|
|
2900
|
+
const graph = readJson(path);
|
|
2901
|
+
const inputHash = expectedInputHash ?? codeGraphInputHash(projectDir, codeIndexSelection(projectDir).files);
|
|
2902
|
+
if (graph.repo_state?.input_hash !== inputHash)
|
|
2903
|
+
return null;
|
|
2904
|
+
return graph;
|
|
2905
|
+
}
|
|
2906
|
+
catch {
|
|
2907
|
+
return null;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
function readCurrentKnowledgeGraph(projectDir, codeGraph, expectedInputHash) {
|
|
2911
|
+
const path = (0, node_path_1.join)(graphDir(projectDir), "graph.json");
|
|
2912
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
2913
|
+
return null;
|
|
2914
|
+
try {
|
|
2915
|
+
const graph = readJson(path);
|
|
2916
|
+
const inputHash = expectedInputHash ?? knowledgeGraphInputHash(projectDir, codeGraph.repo_state.input_hash ?? codeGraphInputHash(projectDir));
|
|
2917
|
+
if (graph.repo_state?.input_hash !== inputHash)
|
|
2918
|
+
return null;
|
|
2919
|
+
return graph;
|
|
2920
|
+
}
|
|
2921
|
+
catch {
|
|
2922
|
+
return null;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
function graphFastFingerprint(projectDir, selection = codeIndexSelection(projectDir)) {
|
|
2926
|
+
const packetPaths = (0, node_fs_1.existsSync)(packetsDir(projectDir))
|
|
2927
|
+
? (0, node_fs_1.readdirSync)(packetsDir(projectDir))
|
|
2928
|
+
.filter((name) => name.endsWith(".json"))
|
|
2929
|
+
.map((name) => (0, node_path_1.join)(packetsDir(projectDir), name))
|
|
2930
|
+
: [];
|
|
2931
|
+
const paths = [
|
|
2932
|
+
...selection.files,
|
|
2933
|
+
...externalIndexFiles(projectDir).map((index) => index.path),
|
|
2934
|
+
...packetPaths,
|
|
2935
|
+
];
|
|
2936
|
+
const entries = paths
|
|
2937
|
+
.filter((path) => (0, node_fs_1.existsSync)(path))
|
|
2938
|
+
.map((path) => {
|
|
2939
|
+
const stats = (0, node_fs_1.statSync)(path);
|
|
2940
|
+
return `${projectRelative(projectDir, path)}:${stats.size}:${Math.round(stats.mtimeMs)}`;
|
|
2941
|
+
})
|
|
2942
|
+
.sort();
|
|
2943
|
+
return sha256Hex(entries.join("\n"));
|
|
2944
|
+
}
|
|
2945
|
+
function readCurrentGraphs(projectDir) {
|
|
2946
|
+
const selection = codeIndexSelection(projectDir);
|
|
2947
|
+
const fingerprint = graphFastFingerprint(projectDir, selection);
|
|
2948
|
+
const cacheKey = (0, node_path_1.resolve)(projectDir);
|
|
2949
|
+
const cached = graphMemoryCache.get(cacheKey);
|
|
2950
|
+
if (cached?.fingerprint === fingerprint) {
|
|
2951
|
+
return { codeGraph: cached.codeGraph, knowledgeGraph: cached.knowledgeGraph };
|
|
2952
|
+
}
|
|
2953
|
+
const codeInputHash = codeGraphInputHash(projectDir, selection.files);
|
|
2954
|
+
const knowledgeInputHash = knowledgeGraphInputHash(projectDir, codeInputHash);
|
|
2955
|
+
if (cached?.codeInputHash === codeInputHash && cached.knowledgeInputHash === knowledgeInputHash) {
|
|
2956
|
+
cached.fingerprint = fingerprint;
|
|
2957
|
+
return { codeGraph: cached.codeGraph, knowledgeGraph: cached.knowledgeGraph };
|
|
2958
|
+
}
|
|
2959
|
+
const codeGraph = readCurrentCodeGraph(projectDir, codeInputHash);
|
|
2960
|
+
if (!codeGraph)
|
|
2961
|
+
return null;
|
|
2962
|
+
const knowledgeGraph = readCurrentKnowledgeGraph(projectDir, codeGraph, knowledgeInputHash);
|
|
2963
|
+
if (!knowledgeGraph)
|
|
2964
|
+
return null;
|
|
2965
|
+
graphMemoryCache.set(cacheKey, { fingerprint, codeInputHash, knowledgeInputHash, codeGraph, knowledgeGraph });
|
|
2966
|
+
return { codeGraph, knowledgeGraph };
|
|
2967
|
+
}
|
|
2968
|
+
function currentOrBuildGraphs(projectDir) {
|
|
2969
|
+
const current = readCurrentGraphs(projectDir);
|
|
2970
|
+
if (current?.codeGraph && current.knowledgeGraph) {
|
|
2971
|
+
return {
|
|
2972
|
+
indexes: [
|
|
2973
|
+
(0, node_path_1.join)(indexesDir(projectDir), "catalog.json"),
|
|
2974
|
+
(0, node_path_1.join)(indexesDir(projectDir), "by-path.json"),
|
|
2975
|
+
(0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
|
|
2976
|
+
(0, node_path_1.join)(indexesDir(projectDir), "by-type.json"),
|
|
2977
|
+
(0, node_path_1.join)(indexesDir(projectDir), "graph.json"),
|
|
2978
|
+
(0, node_path_1.join)(indexesDir(projectDir), "code-graph.json"),
|
|
2979
|
+
],
|
|
2980
|
+
codeGraph: current.codeGraph,
|
|
2981
|
+
knowledgeGraph: current.knowledgeGraph,
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
return buildGraphIndexes(projectDir);
|
|
2985
|
+
}
|
|
2986
|
+
function buildGraphIndexes(projectDir) {
|
|
2987
|
+
const written = buildPacketIndexes(projectDir);
|
|
2988
|
+
const codeGraph = buildCodeGraph(projectDir);
|
|
2989
|
+
const knowledgeGraph = buildKnowledgeGraph(projectDir, codeGraph);
|
|
2990
|
+
const graphIndexPath = (0, node_path_1.join)(indexesDir(projectDir), "graph.json");
|
|
2991
|
+
const codeGraphIndexPath = (0, node_path_1.join)(indexesDir(projectDir), "code-graph.json");
|
|
2992
|
+
writeJson(graphIndexPath, {
|
|
2655
2993
|
schema_version: knowledgeGraph.schema_version,
|
|
2656
2994
|
entities: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(graphDir(projectDir), "entities.json")),
|
|
2657
2995
|
edges: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(graphDir(projectDir), "edges.json")),
|
|
@@ -2660,7 +2998,7 @@ function buildIndexes(projectDir) {
|
|
|
2660
2998
|
edge_count: knowledgeGraph.edges.length,
|
|
2661
2999
|
episode_count: knowledgeGraph.episodes.length,
|
|
2662
3000
|
});
|
|
2663
|
-
writeJson(
|
|
3001
|
+
writeJson(codeGraphIndexPath, {
|
|
2664
3002
|
schema_version: codeGraph.schema_version,
|
|
2665
3003
|
files: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(codeGraphDir(projectDir), "files.json")),
|
|
2666
3004
|
symbols: (0, node_path_1.relative)(projectDir, (0, node_path_1.join)(codeGraphDir(projectDir), "symbols.json")),
|
|
@@ -2676,9 +3014,23 @@ function buildIndexes(projectDir) {
|
|
|
2676
3014
|
route_count: codeGraph.routes.length,
|
|
2677
3015
|
test_count: codeGraph.tests.length,
|
|
2678
3016
|
});
|
|
2679
|
-
|
|
3017
|
+
graphMemoryCache.set((0, node_path_1.resolve)(projectDir), {
|
|
3018
|
+
fingerprint: graphFastFingerprint(projectDir),
|
|
3019
|
+
codeInputHash: codeGraph.repo_state.input_hash ?? "",
|
|
3020
|
+
knowledgeInputHash: knowledgeGraph.repo_state.input_hash ?? "",
|
|
3021
|
+
codeGraph,
|
|
3022
|
+
knowledgeGraph,
|
|
3023
|
+
});
|
|
3024
|
+
return {
|
|
3025
|
+
indexes: [...written, graphIndexPath, codeGraphIndexPath],
|
|
3026
|
+
codeGraph,
|
|
3027
|
+
knowledgeGraph,
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
function buildIndexes(projectDir) {
|
|
3031
|
+
return buildGraphIndexes(projectDir).indexes;
|
|
2680
3032
|
}
|
|
2681
|
-
function
|
|
3033
|
+
function indexProjectDetailed(projectDir, options = {}) {
|
|
2682
3034
|
ensureMemoryDirs(projectDir);
|
|
2683
3035
|
const policy = installAgentPolicy(projectDir);
|
|
2684
3036
|
const migrated = migrateLegacyMarkdown(projectDir);
|
|
@@ -2688,15 +3040,23 @@ function indexProject(projectDir) {
|
|
|
2688
3040
|
const structure = createRepoStructurePacket(projectDir);
|
|
2689
3041
|
if (structure)
|
|
2690
3042
|
upsertGeneratedPacket(projectDir, structure);
|
|
2691
|
-
const
|
|
3043
|
+
const built = options.graphs === false ? null : buildGraphIndexes(projectDir);
|
|
3044
|
+
const indexes = built?.indexes ?? buildPacketIndexes(projectDir);
|
|
2692
3045
|
return {
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
3046
|
+
result: {
|
|
3047
|
+
projectDir,
|
|
3048
|
+
packets: loadPacketsFromDir(packetsDir(projectDir)).length,
|
|
3049
|
+
migrated,
|
|
3050
|
+
indexes: indexes.map((path) => (0, node_path_1.relative)(projectDir, path)),
|
|
3051
|
+
policyPath: (0, node_path_1.relative)(projectDir, policy.path),
|
|
3052
|
+
},
|
|
3053
|
+
codeGraph: built?.codeGraph,
|
|
3054
|
+
knowledgeGraph: built?.knowledgeGraph,
|
|
2698
3055
|
};
|
|
2699
3056
|
}
|
|
3057
|
+
function indexProject(projectDir, options = {}) {
|
|
3058
|
+
return indexProjectDetailed(projectDir, options).result;
|
|
3059
|
+
}
|
|
2700
3060
|
function staleSuggestedAction(reasons) {
|
|
2701
3061
|
if (reasons.some((reason) => reason.includes("status is")))
|
|
2702
3062
|
return "mark_stale";
|
|
@@ -2755,11 +3115,20 @@ function refreshPacketStaleness(projectDir) {
|
|
|
2755
3115
|
return { findings, updated };
|
|
2756
3116
|
}
|
|
2757
3117
|
function refreshProject(projectDir) {
|
|
2758
|
-
const
|
|
3118
|
+
const detailedIndex = indexProjectDetailed(projectDir);
|
|
3119
|
+
const index = detailedIndex.result;
|
|
3120
|
+
let codeGraph = detailedIndex.codeGraph;
|
|
3121
|
+
let knowledgeGraph = detailedIndex.knowledgeGraph;
|
|
2759
3122
|
const stale = refreshPacketStaleness(projectDir);
|
|
2760
|
-
|
|
3123
|
+
let indexes = index.indexes;
|
|
3124
|
+
if (stale.updated > 0) {
|
|
3125
|
+
const rebuilt = buildGraphIndexes(projectDir);
|
|
3126
|
+
codeGraph = rebuilt.codeGraph;
|
|
3127
|
+
knowledgeGraph = rebuilt.knowledgeGraph;
|
|
3128
|
+
indexes = rebuilt.indexes.map((path) => (0, node_path_1.relative)(projectDir, path));
|
|
3129
|
+
}
|
|
2761
3130
|
const validation = validateProject(projectDir);
|
|
2762
|
-
const metrics =
|
|
3131
|
+
const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph, validation });
|
|
2763
3132
|
const nextActions = [];
|
|
2764
3133
|
if (stale.findings.length)
|
|
2765
3134
|
nextActions.push("Update, verify, or supersede stale repo memories before relying on them.");
|
|
@@ -2833,9 +3202,8 @@ function gcProject(projectDir, options = {}) {
|
|
|
2833
3202
|
}
|
|
2834
3203
|
}
|
|
2835
3204
|
if (!options.dryRun && (deprecated.length || deleted.length)) {
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
writeJson((0, node_path_1.join)(memoryRoot(projectDir), "metrics.json"), kageMetrics(projectDir));
|
|
3205
|
+
const rebuilt = buildGraphIndexes(projectDir);
|
|
3206
|
+
writeJson((0, node_path_1.join)(memoryRoot(projectDir), "metrics.json"), kageMetricsShallow(projectDir, rebuilt));
|
|
2839
3207
|
}
|
|
2840
3208
|
return {
|
|
2841
3209
|
ok: true,
|
|
@@ -3084,11 +3452,29 @@ function recallIntentBoost(queryTerms, packet) {
|
|
|
3084
3452
|
score += packet.type === "decision" ? 12 : 0;
|
|
3085
3453
|
return score;
|
|
3086
3454
|
}
|
|
3087
|
-
function
|
|
3088
|
-
const
|
|
3089
|
-
const
|
|
3455
|
+
function recallGraphLookup(graph) {
|
|
3456
|
+
const packetEntityByPacketId = new Map();
|
|
3457
|
+
for (const entity of graph.entities) {
|
|
3458
|
+
if (entity.type !== "memory")
|
|
3459
|
+
continue;
|
|
3460
|
+
for (const alias of entity.aliases)
|
|
3461
|
+
packetEntityByPacketId.set(alias, entity.id);
|
|
3462
|
+
}
|
|
3463
|
+
const edgesByEntityId = new Map();
|
|
3464
|
+
for (const edge of graph.edges) {
|
|
3465
|
+
const from = edgesByEntityId.get(edge.from) ?? [];
|
|
3466
|
+
from.push(edge);
|
|
3467
|
+
edgesByEntityId.set(edge.from, from);
|
|
3468
|
+
const to = edgesByEntityId.get(edge.to) ?? [];
|
|
3469
|
+
to.push(edge);
|
|
3470
|
+
edgesByEntityId.set(edge.to, to);
|
|
3471
|
+
}
|
|
3472
|
+
return { packetEntityByPacketId, edgesByEntityId };
|
|
3473
|
+
}
|
|
3474
|
+
function recallBreakdown(projectDir, terms, packet, textScore, graph = buildKnowledgeGraph(projectDir), lookup = recallGraphLookup(graph)) {
|
|
3475
|
+
const packetEntityId = lookup.packetEntityByPacketId.get(packet.id);
|
|
3090
3476
|
const rawGraphScore = packetEntityId
|
|
3091
|
-
?
|
|
3477
|
+
? (lookup.edgesByEntityId.get(packetEntityId) ?? []).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
|
|
3092
3478
|
: 0;
|
|
3093
3479
|
const graphScore = Math.min(rawGraphScore * 0.45, textScore > 0 ? textScore * 1.5 + 12 : 8);
|
|
3094
3480
|
const pathTypeTag = scoreText(terms, `${packet.type} ${packet.tags.join(" ")} ${packet.paths.join(" ")}`, [packet.type, ...packet.tags, ...packet.paths]);
|
|
@@ -3100,15 +3486,19 @@ function recallBreakdown(projectDir, terms, packet, textScore) {
|
|
|
3100
3486
|
const final = Number((textScore + graphScore + pathTypeTag * 0.8 + intent + vector + freshness + quality + feedback).toFixed(2));
|
|
3101
3487
|
return { bm25: textScore, text: textScore, graph: Number(graphScore.toFixed(2)), path_type_tag: pathTypeTag, intent, vector, freshness, quality: Number(quality.toFixed(2)), feedback, final };
|
|
3102
3488
|
}
|
|
3103
|
-
function recall(projectDir, query, limit = 5, explain = false) {
|
|
3104
|
-
|
|
3489
|
+
function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
|
|
3490
|
+
const current = inputs.codeGraph && inputs.knowledgeGraph ? null : readCurrentGraphs(projectDir);
|
|
3491
|
+
const detailedIndex = inputs.codeGraph && inputs.knowledgeGraph || current ? null : indexProjectDetailed(projectDir);
|
|
3492
|
+
const codeGraph = inputs.codeGraph ?? current?.codeGraph ?? detailedIndex?.codeGraph ?? buildCodeGraph(projectDir);
|
|
3493
|
+
const knowledgeGraph = inputs.knowledgeGraph ?? current?.knowledgeGraph ?? detailedIndex?.knowledgeGraph ?? buildKnowledgeGraph(projectDir, codeGraph);
|
|
3105
3494
|
const terms = tokenize(query);
|
|
3106
3495
|
const approvedPackets = loadApprovedPackets(projectDir);
|
|
3107
3496
|
const lexicalScores = scorePacketsBm25(terms, approvedPackets);
|
|
3497
|
+
const graphLookup = recallGraphLookup(knowledgeGraph);
|
|
3108
3498
|
const scored = approvedPackets
|
|
3109
3499
|
.map((packet) => {
|
|
3110
3500
|
const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
3111
|
-
const score_breakdown = recallBreakdown(projectDir, terms, packet, score);
|
|
3501
|
+
const score_breakdown = recallBreakdown(projectDir, terms, packet, score, knowledgeGraph, graphLookup);
|
|
3112
3502
|
const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
|
|
3113
3503
|
return { packet, score: score_breakdown.final, relevance, why_matched: why, score_breakdown };
|
|
3114
3504
|
})
|
|
@@ -3134,8 +3524,8 @@ function recall(projectDir, query, limit = 5, explain = false) {
|
|
|
3134
3524
|
return true;
|
|
3135
3525
|
})
|
|
3136
3526
|
.slice(0, 3);
|
|
3137
|
-
const graphContext = queryGraph(projectDir, query, 5);
|
|
3138
|
-
const codeContext = queryCodeGraph(projectDir, query, 5);
|
|
3527
|
+
const graphContext = queryGraph(projectDir, query, 5, knowledgeGraph);
|
|
3528
|
+
const codeContext = queryCodeGraph(projectDir, query, 5, codeGraph);
|
|
3139
3529
|
const lines = [
|
|
3140
3530
|
`# Kage Context`,
|
|
3141
3531
|
"",
|
|
@@ -3203,8 +3593,8 @@ function scoreText(terms, text, boosts = []) {
|
|
|
3203
3593
|
score += 3;
|
|
3204
3594
|
return score;
|
|
3205
3595
|
}
|
|
3206
|
-
function queryCodeGraph(projectDir, query, limit = 10) {
|
|
3207
|
-
|
|
3596
|
+
function queryCodeGraph(projectDir, query, limit = 10, graph) {
|
|
3597
|
+
graph = graph ?? readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
|
|
3208
3598
|
const terms = tokenize(query);
|
|
3209
3599
|
const files = graph.files
|
|
3210
3600
|
.map((file) => ({ file, score: scoreText(terms, `${file.path} ${file.kind} ${file.language} ${file.parser}`, [file.path, file.language]) }))
|
|
@@ -3268,8 +3658,8 @@ function queryCodeGraph(projectDir, query, limit = 10) {
|
|
|
3268
3658
|
];
|
|
3269
3659
|
return { query, context_block: lines.join("\n"), files, symbols, imports: imports.map((entry) => entry.item), calls, routes, tests };
|
|
3270
3660
|
}
|
|
3271
|
-
function queryGraph(projectDir, query, limit = 10) {
|
|
3272
|
-
|
|
3661
|
+
function queryGraph(projectDir, query, limit = 10, graph) {
|
|
3662
|
+
graph = graph ?? readCurrentGraphs(projectDir)?.knowledgeGraph ?? buildKnowledgeGraph(projectDir);
|
|
3273
3663
|
const terms = tokenize(query);
|
|
3274
3664
|
const entityScores = new Map();
|
|
3275
3665
|
for (const entity of graph.entities) {
|
|
@@ -3315,7 +3705,7 @@ function mermaidLabel(value) {
|
|
|
3315
3705
|
return value.replace(/["\n\r]/g, " ").slice(0, 80);
|
|
3316
3706
|
}
|
|
3317
3707
|
function graphMermaid(projectDir, limit = 40) {
|
|
3318
|
-
const graph = buildKnowledgeGraph(projectDir);
|
|
3708
|
+
const graph = readCurrentGraphs(projectDir)?.knowledgeGraph ?? buildKnowledgeGraph(projectDir);
|
|
3319
3709
|
const selectedEdges = graph.edges.slice(0, limit);
|
|
3320
3710
|
const selectedEntityIds = new Set(selectedEdges.flatMap((edge) => [edge.from, edge.to]));
|
|
3321
3711
|
const selectedEntities = graph.entities.filter((entity) => selectedEntityIds.has(entity.id));
|
|
@@ -3339,17 +3729,19 @@ function percent(numerator, denominator) {
|
|
|
3339
3729
|
}
|
|
3340
3730
|
function kageMetrics(projectDir) {
|
|
3341
3731
|
ensureMemoryDirs(projectDir);
|
|
3342
|
-
const
|
|
3343
|
-
const
|
|
3732
|
+
const built = currentOrBuildGraphs(projectDir);
|
|
3733
|
+
const codeGraph = built.codeGraph;
|
|
3734
|
+
const knowledgeGraph = built.knowledgeGraph;
|
|
3344
3735
|
const validation = validateProject(projectDir);
|
|
3345
3736
|
const approvedPackets = loadPacketsFromDir(packetsDir(projectDir)).length;
|
|
3346
3737
|
const pendingPackets = loadPacketsFromDir(pendingDir(projectDir)).length;
|
|
3347
3738
|
const evidenceBackedEdges = knowledgeGraph.edges.filter((edge) => edge.evidence.length > 0).length;
|
|
3348
3739
|
const policyPath = (0, node_path_1.join)(projectDir, "AGENTS.md");
|
|
3349
3740
|
const policyInstalled = (0, node_fs_1.existsSync)(policyPath) && (0, node_fs_1.readFileSync)(policyPath, "utf8").includes(AGENTS_POLICY_MARKER);
|
|
3741
|
+
const indexManifest = readCodeIndexManifest(projectDir);
|
|
3350
3742
|
const sourceFiles = codeGraph.files.filter((file) => file.kind === "source" || file.kind === "test");
|
|
3351
3743
|
const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
|
|
3352
|
-
const coverage = percent(indexedSourceFiles.length, sourceFiles.length);
|
|
3744
|
+
const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
|
|
3353
3745
|
const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
|
|
3354
3746
|
const qualityScores = allPackets
|
|
3355
3747
|
.map((packet) => Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score))
|
|
@@ -3371,7 +3763,7 @@ function kageMetrics(projectDir) {
|
|
|
3371
3763
|
(validation.ok ? 5 : -20) -
|
|
3372
3764
|
validation.warnings.length * 2)));
|
|
3373
3765
|
const quality = qualityReport(projectDir);
|
|
3374
|
-
const benchmark = benchmarkProject(projectDir);
|
|
3766
|
+
const benchmark = benchmarkProject(projectDir, { codeGraph, knowledgeGraph });
|
|
3375
3767
|
return {
|
|
3376
3768
|
schema_version: 1,
|
|
3377
3769
|
project_dir: projectDir,
|
|
@@ -3389,6 +3781,13 @@ function kageMetrics(projectDir) {
|
|
|
3389
3781
|
parsers: countBy(codeGraph.files, (file) => file.parser),
|
|
3390
3782
|
source_symbols_by_parser: countBy(codeGraph.symbols, (symbol) => symbol.parser),
|
|
3391
3783
|
indexer_coverage_percent: coverage,
|
|
3784
|
+
index_status: indexManifest.coverage.complete ? "complete" : "partial",
|
|
3785
|
+
indexable_files: indexManifest.coverage.indexable_files || sourceFiles.length,
|
|
3786
|
+
indexed_files: indexManifest.coverage.indexed_files || indexedSourceFiles.length,
|
|
3787
|
+
deferred_files: indexManifest.coverage.deferred_files,
|
|
3788
|
+
ignored_files: indexManifest.coverage.ignored_files,
|
|
3789
|
+
cache_hits: indexManifest.cache.hits,
|
|
3790
|
+
cache_misses: indexManifest.cache.misses,
|
|
3392
3791
|
},
|
|
3393
3792
|
memory_graph: {
|
|
3394
3793
|
approved_packets: approvedPackets,
|
|
@@ -3431,8 +3830,9 @@ function auditProject(projectDir) {
|
|
|
3431
3830
|
ensureMemoryDirs(projectDir);
|
|
3432
3831
|
const validation = validateProject(projectDir);
|
|
3433
3832
|
const quality = qualityReport(projectDir);
|
|
3434
|
-
const
|
|
3435
|
-
const
|
|
3833
|
+
const built = currentOrBuildGraphs(projectDir);
|
|
3834
|
+
const codeGraph = built.codeGraph;
|
|
3835
|
+
const knowledgeGraph = built.knowledgeGraph;
|
|
3436
3836
|
const approved = loadApprovedPackets(projectDir);
|
|
3437
3837
|
const pending = loadPendingPackets(projectDir);
|
|
3438
3838
|
const structuredPackets = approved.filter(hasStructuredEngineeringContext);
|
|
@@ -3681,8 +4081,11 @@ function qualityReport(projectDir) {
|
|
|
3681
4081
|
packets: rows,
|
|
3682
4082
|
};
|
|
3683
4083
|
}
|
|
3684
|
-
function benchmarkProject(projectDir) {
|
|
4084
|
+
function benchmarkProject(projectDir, inputs = {}) {
|
|
3685
4085
|
ensureMemoryDirs(projectDir);
|
|
4086
|
+
const built = inputs.codeGraph && inputs.knowledgeGraph ? null : currentOrBuildGraphs(projectDir);
|
|
4087
|
+
const codeGraph = inputs.codeGraph ?? built?.codeGraph;
|
|
4088
|
+
const knowledgeGraph = inputs.knowledgeGraph ?? built?.knowledgeGraph;
|
|
3686
4089
|
const scenarios = [
|
|
3687
4090
|
{ query: "how do I run tests", expected: "test" },
|
|
3688
4091
|
{ query: "where are routes defined", expected: "route" },
|
|
@@ -3690,7 +4093,7 @@ function benchmarkProject(projectDir) {
|
|
|
3690
4093
|
{ query: "what changed on this branch", expected: "branch" },
|
|
3691
4094
|
{ query: "what gotchas exist", expected: "gotcha" },
|
|
3692
4095
|
].map((scenario) => {
|
|
3693
|
-
const result = recall(projectDir, scenario.query, 5, true);
|
|
4096
|
+
const result = recall(projectDir, scenario.query, 5, true, { codeGraph, knowledgeGraph });
|
|
3694
4097
|
const text = `${result.context_block}\n${result.results.map((entry) => packetText(entry.packet)).join("\n")}`.toLowerCase();
|
|
3695
4098
|
return {
|
|
3696
4099
|
query: scenario.query,
|
|
@@ -3701,7 +4104,7 @@ function benchmarkProject(projectDir) {
|
|
|
3701
4104
|
context_tokens: estimateTokens(result.context_block),
|
|
3702
4105
|
};
|
|
3703
4106
|
});
|
|
3704
|
-
const metrics = kageMetricsShallow(projectDir);
|
|
4107
|
+
const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph });
|
|
3705
4108
|
const quality = qualityReport(projectDir);
|
|
3706
4109
|
const typeCoverage = quality.memory_type_coverage;
|
|
3707
4110
|
const recallHitRate = percent(scenarios.filter((scenario) => scenario.hit).length, scenarios.length);
|
|
@@ -3865,13 +4268,14 @@ function benchmarkTaskComparison(projectDir, task) {
|
|
|
3865
4268
|
],
|
|
3866
4269
|
};
|
|
3867
4270
|
}
|
|
3868
|
-
function kageMetricsShallow(projectDir) {
|
|
3869
|
-
const codeGraph = buildCodeGraph(projectDir);
|
|
3870
|
-
const knowledgeGraph = buildKnowledgeGraph(projectDir);
|
|
3871
|
-
const validation = validateProject(projectDir);
|
|
4271
|
+
function kageMetricsShallow(projectDir, inputs = {}) {
|
|
4272
|
+
const codeGraph = inputs.codeGraph ?? buildCodeGraph(projectDir);
|
|
4273
|
+
const knowledgeGraph = inputs.knowledgeGraph ?? buildKnowledgeGraph(projectDir, codeGraph);
|
|
4274
|
+
const validation = inputs.validation ?? validateProject(projectDir);
|
|
4275
|
+
const indexManifest = readCodeIndexManifest(projectDir);
|
|
3872
4276
|
const sourceFiles = codeGraph.files.filter((file) => file.kind === "source" || file.kind === "test");
|
|
3873
4277
|
const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
|
|
3874
|
-
const coverage = percent(indexedSourceFiles.length, sourceFiles.length);
|
|
4278
|
+
const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
|
|
3875
4279
|
const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
|
|
3876
4280
|
const indexedSourceTokens = Math.ceil(sourceFiles.reduce((sum, file) => sum + file.size_bytes, 0) / 4);
|
|
3877
4281
|
const memoryTokens = allPackets.reduce((sum, packet) => sum + estimateTokens(packetText(packet)), 0);
|
|
@@ -3893,6 +4297,13 @@ function kageMetricsShallow(projectDir) {
|
|
|
3893
4297
|
parsers: countBy(codeGraph.files, (file) => file.parser),
|
|
3894
4298
|
source_symbols_by_parser: countBy(codeGraph.symbols, (symbol) => symbol.parser),
|
|
3895
4299
|
indexer_coverage_percent: coverage,
|
|
4300
|
+
index_status: indexManifest.coverage.complete ? "complete" : "partial",
|
|
4301
|
+
indexable_files: indexManifest.coverage.indexable_files || sourceFiles.length,
|
|
4302
|
+
indexed_files: indexManifest.coverage.indexed_files || indexedSourceFiles.length,
|
|
4303
|
+
deferred_files: indexManifest.coverage.deferred_files,
|
|
4304
|
+
ignored_files: indexManifest.coverage.ignored_files,
|
|
4305
|
+
cache_hits: indexManifest.cache.hits,
|
|
4306
|
+
cache_misses: indexManifest.cache.misses,
|
|
3896
4307
|
},
|
|
3897
4308
|
memory_graph: {
|
|
3898
4309
|
approved_packets: loadPacketsFromDir(packetsDir(projectDir)).length,
|
|
@@ -5578,9 +5989,9 @@ function installClaudeSettings(projectDir) {
|
|
|
5578
5989
|
function initProject(projectDir) {
|
|
5579
5990
|
installAgentPolicy(projectDir);
|
|
5580
5991
|
installClaudeSettings(projectDir);
|
|
5581
|
-
const index = indexProject(projectDir);
|
|
5992
|
+
const index = indexProject(projectDir, { graphs: false });
|
|
5582
5993
|
const validation = validateProject(projectDir);
|
|
5583
|
-
const sampleRecall =
|
|
5994
|
+
const sampleRecall = recallFromPackets("how do I run tests", loadApprovedPackets(projectDir), 5, "Repo Memory");
|
|
5584
5995
|
return { index, validation, sampleRecall };
|
|
5585
5996
|
}
|
|
5586
5997
|
function doctorProject(projectDir) {
|
package/package.json
CHANGED
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; }
|