@kage-core/kage-graph-mcp 1.1.26 → 1.1.28
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 +4 -1
- package/dist/cli.js +34 -0
- package/dist/kernel.js +214 -1
- package/package.json +1 -1
- package/viewer/app.js +251 -0
- package/viewer/index.html +2 -2
- package/viewer/styles.css +117 -0
package/README.md
CHANGED
|
@@ -37,7 +37,8 @@ Restart your agent once after setup so MCP tools reload.
|
|
|
37
37
|
- repo-local memory for decisions, runbooks, bug fixes, gotchas, conventions,
|
|
38
38
|
and code explanations
|
|
39
39
|
- a code graph for files, symbols, imports, calls, routes, tests, and packages,
|
|
40
|
-
including generic call/test signals
|
|
40
|
+
including generic call/test signals and Python framework routes for
|
|
41
|
+
non-TypeScript repos
|
|
41
42
|
- memory-code links so project knowledge points at the code it affects
|
|
42
43
|
- decision intelligence for why-memory coverage, stale/weak packets, and
|
|
43
44
|
important files that still lack linked repo knowledge
|
|
@@ -72,6 +73,7 @@ kage reviewers --project . --changed-files src/auth.ts,src/session.ts --json
|
|
|
72
73
|
kage risk --project . --targets src/auth.ts --json
|
|
73
74
|
kage learn --project . --learning "Use npm test after parser changes."
|
|
74
75
|
kage refresh --project .
|
|
76
|
+
kage hook install --project .
|
|
75
77
|
kage pr check --project .
|
|
76
78
|
kage metrics --project . --json
|
|
77
79
|
kage audit --project . --json
|
|
@@ -133,6 +135,7 @@ Kage is optimized so repeat work scales with changed files, not the whole repo:
|
|
|
133
135
|
- read-only recall reuses fresh graph artifacts
|
|
134
136
|
- unchanged structural file facts are reused
|
|
135
137
|
- generated graphs are compact and avoid duplicated structural JSON
|
|
138
|
+
- optional git `post-commit` hooks keep repo memory and branch summaries current
|
|
136
139
|
- generated/vendor/cache paths are ignored
|
|
137
140
|
- huge files are represented safely instead of deeply expanded
|
|
138
141
|
- recall builds lookup maps once per query instead of repeatedly scanning graph
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,9 @@ Usage:
|
|
|
24
24
|
kage daemon status --project <dir> [--json]
|
|
25
25
|
kage daemon doctor --project <dir> [--json]
|
|
26
26
|
kage viewer --project <dir> [--port 3113]
|
|
27
|
+
kage hook install --project <dir> [--json]
|
|
28
|
+
kage hook status --project <dir> [--json]
|
|
29
|
+
kage hook uninstall --project <dir> [--json]
|
|
27
30
|
kage refresh --project <dir> [--full] [--json]
|
|
28
31
|
kage gc --project <dir> [--dry-run] [--force] [--json]
|
|
29
32
|
kage pr summarize --project <dir> [--json]
|
|
@@ -319,6 +322,37 @@ async function main() {
|
|
|
319
322
|
await (0, daemon_js_1.startViewer)(projectArg(args), { port: numberArg(args, "--port", 3113) });
|
|
320
323
|
return;
|
|
321
324
|
}
|
|
325
|
+
if (command === "hook") {
|
|
326
|
+
const action = args[1];
|
|
327
|
+
const projectDir = projectArg(args);
|
|
328
|
+
const result = action === "install"
|
|
329
|
+
? (0, kernel_js_1.kageHookInstall)(projectDir)
|
|
330
|
+
: action === "status"
|
|
331
|
+
? (0, kernel_js_1.kageHookStatus)(projectDir)
|
|
332
|
+
: action === "uninstall"
|
|
333
|
+
? (0, kernel_js_1.kageHookUninstall)(projectDir)
|
|
334
|
+
: null;
|
|
335
|
+
if (!result)
|
|
336
|
+
usage();
|
|
337
|
+
if (args.includes("--json")) {
|
|
338
|
+
console.log(JSON.stringify(result, null, 2));
|
|
339
|
+
if (!result.ok)
|
|
340
|
+
process.exit(2);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
console.log(result.message);
|
|
344
|
+
if (result.hook_path)
|
|
345
|
+
console.log(`Hook: ${result.hook_path}`);
|
|
346
|
+
console.log(`Installed: ${result.installed ? "yes" : "no"}`);
|
|
347
|
+
console.log(`Changed: ${result.changed ? "yes" : "no"}`);
|
|
348
|
+
if (result.warnings.length)
|
|
349
|
+
console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
|
|
350
|
+
if (result.errors.length)
|
|
351
|
+
console.log(`Errors:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
|
|
352
|
+
if (!result.ok)
|
|
353
|
+
process.exit(2);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
322
356
|
if (command === "gc") {
|
|
323
357
|
const project = projectArg(args);
|
|
324
358
|
const dryRun = args.includes("--dry-run");
|
package/dist/kernel.js
CHANGED
|
@@ -109,6 +109,9 @@ exports.buildBranchOverlay = buildBranchOverlay;
|
|
|
109
109
|
exports.createReviewArtifact = createReviewArtifact;
|
|
110
110
|
exports.prSummarize = prSummarize;
|
|
111
111
|
exports.prCheck = prCheck;
|
|
112
|
+
exports.kageHookStatus = kageHookStatus;
|
|
113
|
+
exports.kageHookInstall = kageHookInstall;
|
|
114
|
+
exports.kageHookUninstall = kageHookUninstall;
|
|
112
115
|
exports.exportPublicBundle = exportPublicBundle;
|
|
113
116
|
exports.orgStatus = orgStatus;
|
|
114
117
|
exports.orgUploadPacket = orgUploadPacket;
|
|
@@ -2827,11 +2830,43 @@ function extractGenericCalls(path, text, symbols, symbolByName) {
|
|
|
2827
2830
|
}
|
|
2828
2831
|
return calls.sort((a, b) => a.line - b.line || a.to_symbol.localeCompare(b.to_symbol));
|
|
2829
2832
|
}
|
|
2833
|
+
function offsetForLine(text, oneBasedLine) {
|
|
2834
|
+
if (oneBasedLine <= 1)
|
|
2835
|
+
return 0;
|
|
2836
|
+
const lines = text.split(/\r?\n/).slice(0, oneBasedLine - 1);
|
|
2837
|
+
return lines.join("\n").length + (lines.length ? 1 : 0);
|
|
2838
|
+
}
|
|
2839
|
+
function normalizeWebRoutePath(routePath) {
|
|
2840
|
+
let cleaned = routePath
|
|
2841
|
+
.trim()
|
|
2842
|
+
.replace(/^r(["'`])|(["'`])$/g, "")
|
|
2843
|
+
.replace(/^['"`]|['"`]$/g, "")
|
|
2844
|
+
.replace(/\\/g, "")
|
|
2845
|
+
.replace(/^\^/, "")
|
|
2846
|
+
.replace(/\$$/, "")
|
|
2847
|
+
.replace(/\{([A-Za-z_][\w]*)\}/g, ":$1")
|
|
2848
|
+
.replace(/<(?:(?:int|str|slug|uuid|path):)?([A-Za-z_][\w]*)>/g, ":$1")
|
|
2849
|
+
.replace(/\/+/g, "/");
|
|
2850
|
+
if (!cleaned.startsWith("/"))
|
|
2851
|
+
cleaned = `/${cleaned}`;
|
|
2852
|
+
if (cleaned.length > 1)
|
|
2853
|
+
cleaned = cleaned.replace(/\/$/, "");
|
|
2854
|
+
return cleaned || "/";
|
|
2855
|
+
}
|
|
2856
|
+
function pythonRouteFramework(text) {
|
|
2857
|
+
return /\bfrom\s+flask\s+import\b|\bimport\s+flask\b|\bFlask\s*\(/.test(text) ? "flask" : "fastapi";
|
|
2858
|
+
}
|
|
2859
|
+
function parsePythonMethodList(value) {
|
|
2860
|
+
if (!value)
|
|
2861
|
+
return ["GET"];
|
|
2862
|
+
const methods = [...value.matchAll(/["']([A-Za-z]+)["']/g)].map((match) => match[1].toUpperCase());
|
|
2863
|
+
return methods.length ? unique(methods) : ["GET"];
|
|
2864
|
+
}
|
|
2830
2865
|
function extractRoutes(path, text, symbols) {
|
|
2831
2866
|
const routes = [];
|
|
2832
2867
|
const addRoute = (method, routePath, offset, framework, handler = null) => {
|
|
2833
2868
|
const line = lineForOffset(text, offset);
|
|
2834
|
-
const cleanRoutePath = routePath
|
|
2869
|
+
const cleanRoutePath = normalizeWebRoutePath(routePath);
|
|
2835
2870
|
const containing = handler ? symbols.find((symbol) => symbol.path === path && symbol.name === handler) : symbolAtLine(symbols, path, line);
|
|
2836
2871
|
routes.push({
|
|
2837
2872
|
id: routeId(path, method, cleanRoutePath, line),
|
|
@@ -2853,6 +2888,34 @@ function extractRoutes(path, text, symbols) {
|
|
|
2853
2888
|
const routeMatch = text.match(new RegExp(`const\\s+${match[2]}Match\\s*=\\s*url\\.pathname\\.match\\(\\s*/\\^\\\\/([^/]+)[^/]*`));
|
|
2854
2889
|
addRoute(match[1], routeMatch ? `/${routeMatch[1]}/:id` : "/:dynamic", match.index ?? 0, "node-http");
|
|
2855
2890
|
}
|
|
2891
|
+
if (extensionOf(path) === ".py") {
|
|
2892
|
+
const lines = text.split(/\r?\n/);
|
|
2893
|
+
const framework = pythonRouteFramework(text);
|
|
2894
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2895
|
+
const line = lines[index];
|
|
2896
|
+
const decorator = line.match(/^\s*@(?:\w+\.)?(get|post|put|patch|delete|options|head)\s*\(\s*["']([^"']+)["']/i);
|
|
2897
|
+
if (decorator) {
|
|
2898
|
+
const handlerLine = lines.slice(index + 1, Math.min(lines.length, index + 7)).find((candidate) => /^\s*(?:async\s+)?def\s+[A-Za-z_][\w]*\s*\(/.test(candidate));
|
|
2899
|
+
const handler = handlerLine?.match(/^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/)?.[1] ?? null;
|
|
2900
|
+
addRoute(decorator[1].toUpperCase(), decorator[2], offsetForLine(text, index + 1), framework, handler);
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
const flaskRoute = line.match(/^\s*@(?:\w+\.)?route\s*\(\s*["']([^"']+)["']/i);
|
|
2904
|
+
if (flaskRoute) {
|
|
2905
|
+
const handlerLine = lines.slice(index + 1, Math.min(lines.length, index + 7)).find((candidate) => /^\s*(?:async\s+)?def\s+[A-Za-z_][\w]*\s*\(/.test(candidate));
|
|
2906
|
+
const handler = handlerLine?.match(/^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/)?.[1] ?? null;
|
|
2907
|
+
const methods = line.match(/methods\s*=\s*\[([^\]]+)\]/i)?.[1];
|
|
2908
|
+
for (const method of parsePythonMethodList(methods))
|
|
2909
|
+
addRoute(method, flaskRoute[1], offsetForLine(text, index + 1), "flask", handler);
|
|
2910
|
+
continue;
|
|
2911
|
+
}
|
|
2912
|
+
const djangoPath = line.match(/\b(?:path|re_path)\s*\(\s*r?["']([^"']+)["']\s*,\s*([A-Za-z_][\w.]+)/);
|
|
2913
|
+
if (djangoPath) {
|
|
2914
|
+
const handler = djangoPath[2].split(".").pop() ?? null;
|
|
2915
|
+
addRoute("ANY", djangoPath[1], offsetForLine(text, index + 1), "django", handler);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2856
2919
|
if (/app\/api\//.test(path)) {
|
|
2857
2920
|
for (const symbol of symbols.filter((symbol) => symbol.path === path && symbol.export && ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(symbol.name))) {
|
|
2858
2921
|
const apiPath = `/${path.replace(/^.*app\/api\//, "").replace(/\/route\.[cm]?[jt]sx?$/, "").replace(/\[([^\]]+)\]/g, ":$1")}`;
|
|
@@ -8217,6 +8280,156 @@ function prCheck(projectDir) {
|
|
|
8217
8280
|
required_actions: requiredActions,
|
|
8218
8281
|
};
|
|
8219
8282
|
}
|
|
8283
|
+
const KAGE_POST_COMMIT_HOOK_START = "# >>> KAGE_POST_COMMIT_HOOK_V1";
|
|
8284
|
+
const KAGE_POST_COMMIT_HOOK_END = "# <<< KAGE_POST_COMMIT_HOOK_V1";
|
|
8285
|
+
function regexpEscape(value) {
|
|
8286
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8287
|
+
}
|
|
8288
|
+
function shellQuote(value) {
|
|
8289
|
+
return `'${value.replace(/'/g, "'\"'\"'")}'`;
|
|
8290
|
+
}
|
|
8291
|
+
function gitHookPath(projectDir) {
|
|
8292
|
+
const raw = readGit(projectDir, ["rev-parse", "--git-path", "hooks/post-commit"]);
|
|
8293
|
+
if (!raw)
|
|
8294
|
+
return null;
|
|
8295
|
+
return (0, node_path_1.resolve)(projectDir, raw);
|
|
8296
|
+
}
|
|
8297
|
+
function hasKageHookBlock(content) {
|
|
8298
|
+
return content.includes(KAGE_POST_COMMIT_HOOK_START) && content.includes(KAGE_POST_COMMIT_HOOK_END);
|
|
8299
|
+
}
|
|
8300
|
+
function stripKageHookBlock(content) {
|
|
8301
|
+
const pattern = new RegExp(`\\n?${regexpEscape(KAGE_POST_COMMIT_HOOK_START)}[\\s\\S]*?${regexpEscape(KAGE_POST_COMMIT_HOOK_END)}\\n?`, "g");
|
|
8302
|
+
const stripped = content.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
8303
|
+
return stripped ? `${stripped}\n` : "";
|
|
8304
|
+
}
|
|
8305
|
+
function kagePostCommitHookBlock(projectDir) {
|
|
8306
|
+
const project = shellQuote((0, node_path_1.resolve)(projectDir));
|
|
8307
|
+
return [
|
|
8308
|
+
KAGE_POST_COMMIT_HOOK_START,
|
|
8309
|
+
"# Kage post-commit hook: keep repo memory and review summary current.",
|
|
8310
|
+
"# Set KAGE_SKIP_HOOK=1 to bypass, or KAGE_BIN=/path/to/kage to override.",
|
|
8311
|
+
"if [ \"${KAGE_SKIP_HOOK:-0}\" != \"1\" ]; then",
|
|
8312
|
+
" KAGE_BIN=\"${KAGE_BIN:-kage}\"",
|
|
8313
|
+
" if command -v \"$KAGE_BIN\" >/dev/null 2>&1; then",
|
|
8314
|
+
" (",
|
|
8315
|
+
` "$KAGE_BIN" refresh --project ${project} --json >/dev/null 2>&1 || true`,
|
|
8316
|
+
` "$KAGE_BIN" pr summarize --project ${project} --json >/dev/null 2>&1 || true`,
|
|
8317
|
+
" ) &",
|
|
8318
|
+
" fi",
|
|
8319
|
+
"fi",
|
|
8320
|
+
KAGE_POST_COMMIT_HOOK_END,
|
|
8321
|
+
].join("\n");
|
|
8322
|
+
}
|
|
8323
|
+
function kageHookStatus(projectDir) {
|
|
8324
|
+
const hookPath = gitHookPath(projectDir);
|
|
8325
|
+
if (!hookPath) {
|
|
8326
|
+
return {
|
|
8327
|
+
ok: false,
|
|
8328
|
+
action: "status",
|
|
8329
|
+
project_dir: projectDir,
|
|
8330
|
+
hook_path: null,
|
|
8331
|
+
installed: false,
|
|
8332
|
+
changed: false,
|
|
8333
|
+
message: "Not a git repository or git is unavailable.",
|
|
8334
|
+
errors: ["Not a git repository or git is unavailable."],
|
|
8335
|
+
warnings: [],
|
|
8336
|
+
};
|
|
8337
|
+
}
|
|
8338
|
+
const content = safeReadText(hookPath) ?? "";
|
|
8339
|
+
const installed = hasKageHookBlock(content);
|
|
8340
|
+
return {
|
|
8341
|
+
ok: true,
|
|
8342
|
+
action: "status",
|
|
8343
|
+
project_dir: projectDir,
|
|
8344
|
+
hook_path: hookPath,
|
|
8345
|
+
installed,
|
|
8346
|
+
changed: false,
|
|
8347
|
+
message: installed ? "Kage post-commit hook is installed." : "Kage post-commit hook is not installed.",
|
|
8348
|
+
errors: [],
|
|
8349
|
+
warnings: (0, node_fs_1.existsSync)(hookPath) ? [] : ["No post-commit hook file exists yet."],
|
|
8350
|
+
};
|
|
8351
|
+
}
|
|
8352
|
+
function kageHookInstall(projectDir) {
|
|
8353
|
+
const hookPath = gitHookPath(projectDir);
|
|
8354
|
+
if (!hookPath) {
|
|
8355
|
+
return {
|
|
8356
|
+
ok: false,
|
|
8357
|
+
action: "install",
|
|
8358
|
+
project_dir: projectDir,
|
|
8359
|
+
hook_path: null,
|
|
8360
|
+
installed: false,
|
|
8361
|
+
changed: false,
|
|
8362
|
+
message: "Not a git repository or git is unavailable.",
|
|
8363
|
+
errors: ["Not a git repository or git is unavailable."],
|
|
8364
|
+
warnings: [],
|
|
8365
|
+
};
|
|
8366
|
+
}
|
|
8367
|
+
ensureDir((0, node_path_1.dirname)(hookPath));
|
|
8368
|
+
const existing = safeReadText(hookPath) ?? "";
|
|
8369
|
+
const base = stripKageHookBlock(existing);
|
|
8370
|
+
const prefix = base.trim() ? base.trimEnd() : "#!/bin/sh";
|
|
8371
|
+
const next = `${prefix}\n\n${kagePostCommitHookBlock(projectDir)}\n`;
|
|
8372
|
+
const changed = existing !== next;
|
|
8373
|
+
if (changed)
|
|
8374
|
+
(0, node_fs_1.writeFileSync)(hookPath, next, "utf8");
|
|
8375
|
+
(0, node_fs_1.chmodSync)(hookPath, 0o755);
|
|
8376
|
+
return {
|
|
8377
|
+
ok: true,
|
|
8378
|
+
action: "install",
|
|
8379
|
+
project_dir: projectDir,
|
|
8380
|
+
hook_path: hookPath,
|
|
8381
|
+
installed: true,
|
|
8382
|
+
changed,
|
|
8383
|
+
message: changed ? "Installed Kage post-commit hook." : "Kage post-commit hook is already current.",
|
|
8384
|
+
errors: [],
|
|
8385
|
+
warnings: [],
|
|
8386
|
+
};
|
|
8387
|
+
}
|
|
8388
|
+
function kageHookUninstall(projectDir) {
|
|
8389
|
+
const hookPath = gitHookPath(projectDir);
|
|
8390
|
+
if (!hookPath) {
|
|
8391
|
+
return {
|
|
8392
|
+
ok: false,
|
|
8393
|
+
action: "uninstall",
|
|
8394
|
+
project_dir: projectDir,
|
|
8395
|
+
hook_path: null,
|
|
8396
|
+
installed: false,
|
|
8397
|
+
changed: false,
|
|
8398
|
+
message: "Not a git repository or git is unavailable.",
|
|
8399
|
+
errors: ["Not a git repository or git is unavailable."],
|
|
8400
|
+
warnings: [],
|
|
8401
|
+
};
|
|
8402
|
+
}
|
|
8403
|
+
const existing = safeReadText(hookPath) ?? "";
|
|
8404
|
+
const installed = hasKageHookBlock(existing);
|
|
8405
|
+
if (!installed) {
|
|
8406
|
+
return {
|
|
8407
|
+
ok: true,
|
|
8408
|
+
action: "uninstall",
|
|
8409
|
+
project_dir: projectDir,
|
|
8410
|
+
hook_path: hookPath,
|
|
8411
|
+
installed: false,
|
|
8412
|
+
changed: false,
|
|
8413
|
+
message: "Kage post-commit hook was not installed.",
|
|
8414
|
+
errors: [],
|
|
8415
|
+
warnings: (0, node_fs_1.existsSync)(hookPath) ? [] : ["No post-commit hook file exists."],
|
|
8416
|
+
};
|
|
8417
|
+
}
|
|
8418
|
+
const next = stripKageHookBlock(existing);
|
|
8419
|
+
(0, node_fs_1.writeFileSync)(hookPath, next, "utf8");
|
|
8420
|
+
(0, node_fs_1.chmodSync)(hookPath, 0o755);
|
|
8421
|
+
return {
|
|
8422
|
+
ok: true,
|
|
8423
|
+
action: "uninstall",
|
|
8424
|
+
project_dir: projectDir,
|
|
8425
|
+
hook_path: hookPath,
|
|
8426
|
+
installed: false,
|
|
8427
|
+
changed: true,
|
|
8428
|
+
message: "Removed Kage post-commit hook.",
|
|
8429
|
+
errors: [],
|
|
8430
|
+
warnings: [],
|
|
8431
|
+
};
|
|
8432
|
+
}
|
|
8220
8433
|
function exportPublicBundle(projectDir) {
|
|
8221
8434
|
ensureMemoryDirs(projectDir);
|
|
8222
8435
|
const candidates = loadPacketsFromDir(publicCandidatesDir(projectDir));
|
package/package.json
CHANGED
package/viewer/app.js
CHANGED
|
@@ -2252,6 +2252,257 @@
|
|
|
2252
2252
|
});
|
|
2253
2253
|
els.intelligenceList.appendChild(item);
|
|
2254
2254
|
});
|
|
2255
|
+
var sections = buildIntelligenceSections(reports);
|
|
2256
|
+
if (sections.length) {
|
|
2257
|
+
var grid = document.createElement("div");
|
|
2258
|
+
grid.className = "intel-deep-grid";
|
|
2259
|
+
sections.forEach(function (section) {
|
|
2260
|
+
grid.appendChild(renderIntelligenceSection(section));
|
|
2261
|
+
});
|
|
2262
|
+
els.intelligenceList.appendChild(grid);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function renderIntelligenceSection(section) {
|
|
2267
|
+
var panel = document.createElement("article");
|
|
2268
|
+
panel.className = "intel-section";
|
|
2269
|
+
var header = document.createElement("div");
|
|
2270
|
+
header.className = "intel-section-header";
|
|
2271
|
+
header.innerHTML = "<div><h3></h3><span></span></div><strong></strong>";
|
|
2272
|
+
header.querySelector("h3").textContent = section.title;
|
|
2273
|
+
header.querySelector("span").textContent = section.kicker || "";
|
|
2274
|
+
header.querySelector("strong").textContent = section.stat || "";
|
|
2275
|
+
panel.appendChild(header);
|
|
2276
|
+
if (section.summary) {
|
|
2277
|
+
var summary = document.createElement("p");
|
|
2278
|
+
summary.className = "intel-section-summary";
|
|
2279
|
+
summary.textContent = section.summary;
|
|
2280
|
+
panel.appendChild(summary);
|
|
2281
|
+
}
|
|
2282
|
+
var list = document.createElement("div");
|
|
2283
|
+
list.className = "intel-section-list";
|
|
2284
|
+
section.rows.slice(0, section.limit || 8).forEach(function (row) {
|
|
2285
|
+
var button = document.createElement("button");
|
|
2286
|
+
button.type = "button";
|
|
2287
|
+
button.className = classNames("intel-row", row.status && "intel-row-" + safeCssName(row.status), row.path && "clickable");
|
|
2288
|
+
button.innerHTML = [
|
|
2289
|
+
"<span class=\"intel-row-main\"><strong></strong><em></em></span>",
|
|
2290
|
+
"<span class=\"intel-row-meta\"></span>",
|
|
2291
|
+
"<span class=\"intel-row-bar\"><i></i></span>"
|
|
2292
|
+
].join("");
|
|
2293
|
+
button.querySelector("strong").textContent = row.label || "";
|
|
2294
|
+
button.querySelector("em").textContent = row.value || "";
|
|
2295
|
+
button.querySelector(".intel-row-meta").textContent = row.meta || "";
|
|
2296
|
+
button.querySelector(".intel-row-bar i").style.width = clamp(Number(row.score || 0), 4, 100) + "%";
|
|
2297
|
+
if (row.path) {
|
|
2298
|
+
button.title = "Focus " + row.path + " in the graph";
|
|
2299
|
+
button.addEventListener("click", function () {
|
|
2300
|
+
focusGraphPath(row.path);
|
|
2301
|
+
});
|
|
2302
|
+
} else {
|
|
2303
|
+
button.disabled = true;
|
|
2304
|
+
}
|
|
2305
|
+
list.appendChild(button);
|
|
2306
|
+
});
|
|
2307
|
+
panel.appendChild(list);
|
|
2308
|
+
return panel;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
function buildIntelligenceSections(reports) {
|
|
2312
|
+
var sections = [];
|
|
2313
|
+
var contributors = reports.contributors;
|
|
2314
|
+
var risk = reports.risk;
|
|
2315
|
+
var decisions = reports.decisions;
|
|
2316
|
+
var health = reports.moduleHealth;
|
|
2317
|
+
var insights = reports.graphInsights;
|
|
2318
|
+
|
|
2319
|
+
if (contributors || risk) {
|
|
2320
|
+
var profiles = contributors && Array.isArray(contributors.contributors) ? contributors.contributors : [];
|
|
2321
|
+
var silos = risk && Array.isArray(risk.ownership_silos) ? risk.ownership_silos : [];
|
|
2322
|
+
var maxOwned = Math.max(1, profiles.reduce(function (max, profile) {
|
|
2323
|
+
return Math.max(max, Number(profile.primary_owned_files || 0));
|
|
2324
|
+
}, 0));
|
|
2325
|
+
var rows = profiles.slice(0, 6).map(function (profile) {
|
|
2326
|
+
var owned = Number(profile.primary_owned_files || 0);
|
|
2327
|
+
return {
|
|
2328
|
+
label: shortContributor(profile.contributor),
|
|
2329
|
+
value: owned + " owned",
|
|
2330
|
+
meta: profile.commits_90d + " commits in 90d, " + (profile.silo_files ? profile.silo_files.length : 0) + " silo files",
|
|
2331
|
+
score: owned / maxOwned * 100,
|
|
2332
|
+
status: owned > 0 && profile.silo_files && profile.silo_files.length ? "warn" : "ok",
|
|
2333
|
+
};
|
|
2334
|
+
}).concat(silos.slice(0, 4).map(function (silo) {
|
|
2335
|
+
return {
|
|
2336
|
+
label: silo.file_path,
|
|
2337
|
+
value: Math.round(Number(silo.primary_owner_pct || 0) * 100) + "% owner",
|
|
2338
|
+
meta: shortContributor(silo.primary_owner || "unknown") + ", " + (silo.commit_count_total || 0) + " commits",
|
|
2339
|
+
score: Number(silo.primary_owner_pct || 0) * 100,
|
|
2340
|
+
status: "warn",
|
|
2341
|
+
path: silo.file_path,
|
|
2342
|
+
};
|
|
2343
|
+
}));
|
|
2344
|
+
if (rows.length) {
|
|
2345
|
+
sections.push({
|
|
2346
|
+
title: "Ownership Map",
|
|
2347
|
+
kicker: "who owns what",
|
|
2348
|
+
stat: silos.length + " silos",
|
|
2349
|
+
summary: "Repowise has a dedicated ownership page. Kage now surfaces the same reviewer-critical signal inside the memory viewer, tied back to selectable files.",
|
|
2350
|
+
rows: rows,
|
|
2351
|
+
limit: 10,
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
if (health && Array.isArray(health.modules)) {
|
|
2357
|
+
var modules = health.modules.slice().sort(function (a, b) {
|
|
2358
|
+
return Number(a.score || 0) - Number(b.score || 0) || String(a.module).localeCompare(String(b.module));
|
|
2359
|
+
});
|
|
2360
|
+
sections.push({
|
|
2361
|
+
title: "Module Health Map",
|
|
2362
|
+
kicker: "churn / tests / ownership",
|
|
2363
|
+
stat: modules.length + " modules",
|
|
2364
|
+
summary: "Lowest-scoring modules are shown first so the viewer points people toward the riskiest areas instead of only drawing nodes.",
|
|
2365
|
+
rows: modules.slice(0, 8).map(function (item) {
|
|
2366
|
+
return {
|
|
2367
|
+
label: item.module,
|
|
2368
|
+
value: item.grade + " / " + item.score,
|
|
2369
|
+
meta: Array.isArray(item.reasons) ? item.reasons.slice(0, 2).join("; ") : "",
|
|
2370
|
+
score: Number(item.score || 0),
|
|
2371
|
+
status: Number(item.score || 0) < 60 ? "danger" : Number(item.score || 0) < 75 ? "warn" : "ok",
|
|
2372
|
+
};
|
|
2373
|
+
}),
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
if (decisions && Array.isArray(decisions.coverage_gaps)) {
|
|
2378
|
+
var gaps = decisions.coverage_gaps;
|
|
2379
|
+
sections.push({
|
|
2380
|
+
title: "Onboarding Targets",
|
|
2381
|
+
kicker: "missing repo lore",
|
|
2382
|
+
stat: (decisions.coverage_percent != null ? decisions.coverage_percent + "%" : "n/a"),
|
|
2383
|
+
summary: "Files with centrality, churn, test gaps, or ownership but no linked why-memory. These are the places a future agent is most likely to rediscover context.",
|
|
2384
|
+
rows: gaps.slice(0, 8).map(function (gap) {
|
|
2385
|
+
var score = Math.min(100, Number(gap.dependents || 0) * 18 + Number(gap.churn_90d || 0) * 6 + 12);
|
|
2386
|
+
return {
|
|
2387
|
+
label: gap.path,
|
|
2388
|
+
value: "needs memory",
|
|
2389
|
+
meta: gap.reason || "",
|
|
2390
|
+
score: score,
|
|
2391
|
+
status: "warn",
|
|
2392
|
+
path: gap.path,
|
|
2393
|
+
};
|
|
2394
|
+
}),
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
if (insights && Array.isArray(insights.communities)) {
|
|
2399
|
+
var communities = insights.communities.slice().sort(function (a, b) {
|
|
2400
|
+
return (b.files ? b.files.length : 0) - (a.files ? a.files.length : 0);
|
|
2401
|
+
});
|
|
2402
|
+
var maxFiles = Math.max(1, communities.reduce(function (max, community) {
|
|
2403
|
+
return Math.max(max, community.files ? community.files.length : 0);
|
|
2404
|
+
}, 0));
|
|
2405
|
+
sections.push({
|
|
2406
|
+
title: "Architecture Communities",
|
|
2407
|
+
kicker: "module clusters",
|
|
2408
|
+
stat: communities.length + " clusters",
|
|
2409
|
+
summary: "Repowise exposes architecture/community views. Kage can show the same high-level clusters next to the memory-code graph so users know what a dense graph means.",
|
|
2410
|
+
rows: communities.slice(0, 8).map(function (community) {
|
|
2411
|
+
var files = community.files || [];
|
|
2412
|
+
var entrypoints = community.entrypoints || [];
|
|
2413
|
+
var routes = community.routes || [];
|
|
2414
|
+
return {
|
|
2415
|
+
label: community.label || ("community " + community.id),
|
|
2416
|
+
value: files.length + " files",
|
|
2417
|
+
meta: entrypoints.length + " entrypoints, " + routes.length + " routes",
|
|
2418
|
+
score: files.length / maxFiles * 100,
|
|
2419
|
+
status: routes.length || entrypoints.length ? "ok" : "",
|
|
2420
|
+
path: files[0],
|
|
2421
|
+
};
|
|
2422
|
+
}),
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
if (insights && Array.isArray(insights.entry_flows) && insights.entry_flows.length) {
|
|
2427
|
+
sections.push({
|
|
2428
|
+
title: "Execution Flows",
|
|
2429
|
+
kicker: "entrypoint traces",
|
|
2430
|
+
stat: insights.entry_flows.length + " flows",
|
|
2431
|
+
summary: "Short traces make the code graph explainable: where execution starts, what it crosses, and which file to inspect first.",
|
|
2432
|
+
rows: insights.entry_flows.slice(0, 8).map(function (flow) {
|
|
2433
|
+
var path = flow.path || [];
|
|
2434
|
+
return {
|
|
2435
|
+
label: flow.entry,
|
|
2436
|
+
value: Math.max(0, path.length - 1) + " hops",
|
|
2437
|
+
meta: path.slice(0, 5).join(" -> "),
|
|
2438
|
+
score: Math.min(100, Math.max(12, path.length * 18)),
|
|
2439
|
+
status: "ok",
|
|
2440
|
+
path: flow.entry,
|
|
2441
|
+
};
|
|
2442
|
+
}),
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
if (risk) {
|
|
2447
|
+
var targets = Array.isArray(risk.targets) ? risk.targets : Object.keys(risk.targets || {}).map(function (key) { return risk.targets[key]; });
|
|
2448
|
+
var hotspots = Array.isArray(risk.global_hotspots) ? risk.global_hotspots : [];
|
|
2449
|
+
var riskRows = targets.slice(0, 6).map(function (item) {
|
|
2450
|
+
return {
|
|
2451
|
+
label: item.target,
|
|
2452
|
+
value: item.risk_type || "risk",
|
|
2453
|
+
meta: item.risk_summary || "",
|
|
2454
|
+
score: Math.round(Number(item.hotspot_score || 0) * 100) || Math.min(100, Number(item.dependents_count || 0) * 12 + (item.test_gap ? 24 : 0)),
|
|
2455
|
+
status: item.test_gap || item.risk_type === "single-owner" ? "warn" : "",
|
|
2456
|
+
path: item.target,
|
|
2457
|
+
};
|
|
2458
|
+
}).concat(hotspots.slice(0, 4).map(function (hotspot) {
|
|
2459
|
+
return {
|
|
2460
|
+
label: hotspot.file_path,
|
|
2461
|
+
value: Math.round(Number(hotspot.hotspot_score || 0) * 100) + "% hot",
|
|
2462
|
+
meta: (hotspot.commit_count_90d || 0) + " commits in 90d, owner " + shortContributor(hotspot.primary_owner || "unknown"),
|
|
2463
|
+
score: Math.round(Number(hotspot.hotspot_score || 0) * 100),
|
|
2464
|
+
status: "danger",
|
|
2465
|
+
path: hotspot.file_path,
|
|
2466
|
+
};
|
|
2467
|
+
}));
|
|
2468
|
+
if (riskRows.length) {
|
|
2469
|
+
sections.push({
|
|
2470
|
+
title: "Blast Radius",
|
|
2471
|
+
kicker: "change impact",
|
|
2472
|
+
stat: riskRows.length + " signals",
|
|
2473
|
+
summary: "Change risk is useful only if it is browsable. Rows focus the graph on affected files when the graph node exists.",
|
|
2474
|
+
rows: riskRows,
|
|
2475
|
+
limit: 10,
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
return sections.filter(function (section) { return section.rows && section.rows.length; });
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
function focusGraphPath(path) {
|
|
2484
|
+
if (!path) return;
|
|
2485
|
+
var normalized = String(path).replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2486
|
+
var found = state.entities.find(function (entity) {
|
|
2487
|
+
var entityPath = String(entity.path || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2488
|
+
return entityPath === normalized || entity.id === normalized || entity.id === "code:file:" + normalized;
|
|
2489
|
+
}) || state.entities.find(function (entity) {
|
|
2490
|
+
var text = [entity.id, entity.path, entity.name].filter(Boolean).join(" ");
|
|
2491
|
+
return text.indexOf(normalized) !== -1;
|
|
2492
|
+
});
|
|
2493
|
+
if (!found) {
|
|
2494
|
+
els.searchInput.value = normalized;
|
|
2495
|
+
scheduleRender();
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
state.selected = { kind: "entity", id: found.id };
|
|
2499
|
+
els.searchInput.value = normalized;
|
|
2500
|
+
scheduleRender();
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
function shortContributor(value) {
|
|
2504
|
+
var text = String(value || "unknown");
|
|
2505
|
+
return text.replace(/\s*<[^>]+>\s*$/, "");
|
|
2255
2506
|
}
|
|
2256
2507
|
|
|
2257
2508
|
function buildIntelligenceCards(reports) {
|
package/viewer/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Kage Memory Terminal</title>
|
|
7
|
-
<link rel="stylesheet" href="./styles.css?v=
|
|
7
|
+
<link rel="stylesheet" href="./styles.css?v=21">
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<header class="app-header">
|
|
@@ -171,6 +171,6 @@
|
|
|
171
171
|
</section>
|
|
172
172
|
</main>
|
|
173
173
|
|
|
174
|
-
<script src="./app.js?v=
|
|
174
|
+
<script src="./app.js?v=21"></script>
|
|
175
175
|
</body>
|
|
176
176
|
</html>
|
package/viewer/styles.css
CHANGED
|
@@ -608,6 +608,12 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
608
608
|
gap: 8px;
|
|
609
609
|
align-content: start;
|
|
610
610
|
}
|
|
611
|
+
.intel-deep-grid {
|
|
612
|
+
grid-column: 1 / -1;
|
|
613
|
+
display: grid;
|
|
614
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
615
|
+
gap: 8px;
|
|
616
|
+
}
|
|
611
617
|
.intel-card {
|
|
612
618
|
min-height: 160px;
|
|
613
619
|
padding: 11px;
|
|
@@ -648,6 +654,114 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
648
654
|
overflow-wrap: anywhere;
|
|
649
655
|
}
|
|
650
656
|
.intel-card strong { color: var(--terminal-strong); }
|
|
657
|
+
.intel-section {
|
|
658
|
+
min-height: 230px;
|
|
659
|
+
padding: 11px;
|
|
660
|
+
border: 1px solid rgba(65, 255, 143, 0.24);
|
|
661
|
+
border-radius: 4px;
|
|
662
|
+
background: linear-gradient(180deg, rgba(13, 25, 19, 0.96), rgba(5, 11, 8, 0.96));
|
|
663
|
+
overflow: hidden;
|
|
664
|
+
}
|
|
665
|
+
.intel-section-header {
|
|
666
|
+
display: flex;
|
|
667
|
+
align-items: flex-start;
|
|
668
|
+
justify-content: space-between;
|
|
669
|
+
gap: 10px;
|
|
670
|
+
}
|
|
671
|
+
.intel-section-header h3 {
|
|
672
|
+
margin: 0;
|
|
673
|
+
color: var(--terminal);
|
|
674
|
+
font-size: 12px;
|
|
675
|
+
letter-spacing: 0;
|
|
676
|
+
}
|
|
677
|
+
.intel-section-header span {
|
|
678
|
+
display: block;
|
|
679
|
+
margin-top: 3px;
|
|
680
|
+
color: var(--terminal-dim);
|
|
681
|
+
font-size: 10px;
|
|
682
|
+
font-weight: 760;
|
|
683
|
+
text-transform: uppercase;
|
|
684
|
+
}
|
|
685
|
+
.intel-section-header strong {
|
|
686
|
+
flex: 0 0 auto;
|
|
687
|
+
color: var(--terminal-strong);
|
|
688
|
+
font-size: 11px;
|
|
689
|
+
}
|
|
690
|
+
.intel-section-summary {
|
|
691
|
+
margin: 8px 0 10px;
|
|
692
|
+
color: var(--muted);
|
|
693
|
+
font-size: 11px;
|
|
694
|
+
overflow-wrap: anywhere;
|
|
695
|
+
}
|
|
696
|
+
.intel-section-list {
|
|
697
|
+
display: grid;
|
|
698
|
+
gap: 7px;
|
|
699
|
+
}
|
|
700
|
+
.intel-row {
|
|
701
|
+
width: 100%;
|
|
702
|
+
display: grid;
|
|
703
|
+
grid-template-columns: minmax(0, 1fr);
|
|
704
|
+
gap: 5px;
|
|
705
|
+
min-height: 0;
|
|
706
|
+
padding: 8px;
|
|
707
|
+
border-color: var(--line);
|
|
708
|
+
background: rgba(3, 6, 4, 0.55);
|
|
709
|
+
color: var(--text);
|
|
710
|
+
text-align: left;
|
|
711
|
+
white-space: normal;
|
|
712
|
+
box-shadow: none;
|
|
713
|
+
cursor: default;
|
|
714
|
+
}
|
|
715
|
+
.intel-row.clickable { cursor: pointer; }
|
|
716
|
+
.intel-row.clickable:hover {
|
|
717
|
+
border-color: var(--terminal-strong);
|
|
718
|
+
background: rgba(65, 255, 143, 0.075);
|
|
719
|
+
}
|
|
720
|
+
.intel-row-main {
|
|
721
|
+
display: flex;
|
|
722
|
+
align-items: baseline;
|
|
723
|
+
justify-content: space-between;
|
|
724
|
+
gap: 8px;
|
|
725
|
+
min-width: 0;
|
|
726
|
+
}
|
|
727
|
+
.intel-row-main strong {
|
|
728
|
+
min-width: 0;
|
|
729
|
+
color: var(--text);
|
|
730
|
+
font-size: 11px;
|
|
731
|
+
overflow: hidden;
|
|
732
|
+
text-overflow: ellipsis;
|
|
733
|
+
white-space: nowrap;
|
|
734
|
+
}
|
|
735
|
+
.intel-row-main em {
|
|
736
|
+
flex: 0 0 auto;
|
|
737
|
+
color: var(--terminal-strong);
|
|
738
|
+
font-size: 10px;
|
|
739
|
+
font-style: normal;
|
|
740
|
+
}
|
|
741
|
+
.intel-row-meta {
|
|
742
|
+
color: var(--terminal-dim);
|
|
743
|
+
font-size: 10px;
|
|
744
|
+
overflow: hidden;
|
|
745
|
+
text-overflow: ellipsis;
|
|
746
|
+
white-space: nowrap;
|
|
747
|
+
}
|
|
748
|
+
.intel-row-bar {
|
|
749
|
+
display: block;
|
|
750
|
+
height: 4px;
|
|
751
|
+
overflow: hidden;
|
|
752
|
+
border-radius: 999px;
|
|
753
|
+
background: rgba(65, 255, 143, 0.10);
|
|
754
|
+
}
|
|
755
|
+
.intel-row-bar i {
|
|
756
|
+
display: block;
|
|
757
|
+
height: 100%;
|
|
758
|
+
border-radius: inherit;
|
|
759
|
+
background: var(--terminal-strong);
|
|
760
|
+
}
|
|
761
|
+
.intel-row-warn .intel-row-main em,
|
|
762
|
+
.intel-row-warn .intel-row-bar i { color: var(--warn); background: var(--warn); }
|
|
763
|
+
.intel-row-danger .intel-row-main em,
|
|
764
|
+
.intel-row-danger .intel-row-bar i { color: var(--danger); background: var(--danger); }
|
|
651
765
|
.review-item {
|
|
652
766
|
padding: 10px 0;
|
|
653
767
|
border-top: 1px solid var(--line);
|
|
@@ -821,6 +935,8 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
821
935
|
.control-panel label, .control-panel button { margin-top: 0; }
|
|
822
936
|
.graph-panel { min-height: 620px; }
|
|
823
937
|
#graphCanvas, .three-graph, .graph-canvas-wrap, #graphSvg { min-height: 560px; }
|
|
938
|
+
.intelligence-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
939
|
+
.intel-deep-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
824
940
|
.details-panel {
|
|
825
941
|
height: auto;
|
|
826
942
|
max-height: 70vh;
|
|
@@ -835,6 +951,7 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
835
951
|
.metrics-grid { grid-template-columns: 1fr 1fr; }
|
|
836
952
|
.proof-list { grid-template-columns: 1fr 1fr; }
|
|
837
953
|
.intelligence-list { grid-template-columns: 1fr; }
|
|
954
|
+
.intel-deep-grid { grid-template-columns: 1fr; }
|
|
838
955
|
.graph-toolbar { align-items: flex-start; flex-direction: column; }
|
|
839
956
|
.graph-actions { width: 100%; }
|
|
840
957
|
.interaction-hint { flex: 1; white-space: normal; }
|