@kage-core/kage-graph-mcp 1.1.26 → 1.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,6 +72,7 @@ kage reviewers --project . --changed-files src/auth.ts,src/session.ts --json
72
72
  kage risk --project . --targets src/auth.ts --json
73
73
  kage learn --project . --learning "Use npm test after parser changes."
74
74
  kage refresh --project .
75
+ kage hook install --project .
75
76
  kage pr check --project .
76
77
  kage metrics --project . --json
77
78
  kage audit --project . --json
@@ -133,6 +134,7 @@ Kage is optimized so repeat work scales with changed files, not the whole repo:
133
134
  - read-only recall reuses fresh graph artifacts
134
135
  - unchanged structural file facts are reused
135
136
  - generated graphs are compact and avoid duplicated structural JSON
137
+ - optional git `post-commit` hooks keep repo memory and branch summaries current
136
138
  - generated/vendor/cache paths are ignored
137
139
  - huge files are represented safely instead of deeply expanded
138
140
  - 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;
@@ -8217,6 +8220,156 @@ function prCheck(projectDir) {
8217
8220
  required_actions: requiredActions,
8218
8221
  };
8219
8222
  }
8223
+ const KAGE_POST_COMMIT_HOOK_START = "# >>> KAGE_POST_COMMIT_HOOK_V1";
8224
+ const KAGE_POST_COMMIT_HOOK_END = "# <<< KAGE_POST_COMMIT_HOOK_V1";
8225
+ function regexpEscape(value) {
8226
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8227
+ }
8228
+ function shellQuote(value) {
8229
+ return `'${value.replace(/'/g, "'\"'\"'")}'`;
8230
+ }
8231
+ function gitHookPath(projectDir) {
8232
+ const raw = readGit(projectDir, ["rev-parse", "--git-path", "hooks/post-commit"]);
8233
+ if (!raw)
8234
+ return null;
8235
+ return (0, node_path_1.resolve)(projectDir, raw);
8236
+ }
8237
+ function hasKageHookBlock(content) {
8238
+ return content.includes(KAGE_POST_COMMIT_HOOK_START) && content.includes(KAGE_POST_COMMIT_HOOK_END);
8239
+ }
8240
+ function stripKageHookBlock(content) {
8241
+ const pattern = new RegExp(`\\n?${regexpEscape(KAGE_POST_COMMIT_HOOK_START)}[\\s\\S]*?${regexpEscape(KAGE_POST_COMMIT_HOOK_END)}\\n?`, "g");
8242
+ const stripped = content.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").trimEnd();
8243
+ return stripped ? `${stripped}\n` : "";
8244
+ }
8245
+ function kagePostCommitHookBlock(projectDir) {
8246
+ const project = shellQuote((0, node_path_1.resolve)(projectDir));
8247
+ return [
8248
+ KAGE_POST_COMMIT_HOOK_START,
8249
+ "# Kage post-commit hook: keep repo memory and review summary current.",
8250
+ "# Set KAGE_SKIP_HOOK=1 to bypass, or KAGE_BIN=/path/to/kage to override.",
8251
+ "if [ \"${KAGE_SKIP_HOOK:-0}\" != \"1\" ]; then",
8252
+ " KAGE_BIN=\"${KAGE_BIN:-kage}\"",
8253
+ " if command -v \"$KAGE_BIN\" >/dev/null 2>&1; then",
8254
+ " (",
8255
+ ` "$KAGE_BIN" refresh --project ${project} --json >/dev/null 2>&1 || true`,
8256
+ ` "$KAGE_BIN" pr summarize --project ${project} --json >/dev/null 2>&1 || true`,
8257
+ " ) &",
8258
+ " fi",
8259
+ "fi",
8260
+ KAGE_POST_COMMIT_HOOK_END,
8261
+ ].join("\n");
8262
+ }
8263
+ function kageHookStatus(projectDir) {
8264
+ const hookPath = gitHookPath(projectDir);
8265
+ if (!hookPath) {
8266
+ return {
8267
+ ok: false,
8268
+ action: "status",
8269
+ project_dir: projectDir,
8270
+ hook_path: null,
8271
+ installed: false,
8272
+ changed: false,
8273
+ message: "Not a git repository or git is unavailable.",
8274
+ errors: ["Not a git repository or git is unavailable."],
8275
+ warnings: [],
8276
+ };
8277
+ }
8278
+ const content = safeReadText(hookPath) ?? "";
8279
+ const installed = hasKageHookBlock(content);
8280
+ return {
8281
+ ok: true,
8282
+ action: "status",
8283
+ project_dir: projectDir,
8284
+ hook_path: hookPath,
8285
+ installed,
8286
+ changed: false,
8287
+ message: installed ? "Kage post-commit hook is installed." : "Kage post-commit hook is not installed.",
8288
+ errors: [],
8289
+ warnings: (0, node_fs_1.existsSync)(hookPath) ? [] : ["No post-commit hook file exists yet."],
8290
+ };
8291
+ }
8292
+ function kageHookInstall(projectDir) {
8293
+ const hookPath = gitHookPath(projectDir);
8294
+ if (!hookPath) {
8295
+ return {
8296
+ ok: false,
8297
+ action: "install",
8298
+ project_dir: projectDir,
8299
+ hook_path: null,
8300
+ installed: false,
8301
+ changed: false,
8302
+ message: "Not a git repository or git is unavailable.",
8303
+ errors: ["Not a git repository or git is unavailable."],
8304
+ warnings: [],
8305
+ };
8306
+ }
8307
+ ensureDir((0, node_path_1.dirname)(hookPath));
8308
+ const existing = safeReadText(hookPath) ?? "";
8309
+ const base = stripKageHookBlock(existing);
8310
+ const prefix = base.trim() ? base.trimEnd() : "#!/bin/sh";
8311
+ const next = `${prefix}\n\n${kagePostCommitHookBlock(projectDir)}\n`;
8312
+ const changed = existing !== next;
8313
+ if (changed)
8314
+ (0, node_fs_1.writeFileSync)(hookPath, next, "utf8");
8315
+ (0, node_fs_1.chmodSync)(hookPath, 0o755);
8316
+ return {
8317
+ ok: true,
8318
+ action: "install",
8319
+ project_dir: projectDir,
8320
+ hook_path: hookPath,
8321
+ installed: true,
8322
+ changed,
8323
+ message: changed ? "Installed Kage post-commit hook." : "Kage post-commit hook is already current.",
8324
+ errors: [],
8325
+ warnings: [],
8326
+ };
8327
+ }
8328
+ function kageHookUninstall(projectDir) {
8329
+ const hookPath = gitHookPath(projectDir);
8330
+ if (!hookPath) {
8331
+ return {
8332
+ ok: false,
8333
+ action: "uninstall",
8334
+ project_dir: projectDir,
8335
+ hook_path: null,
8336
+ installed: false,
8337
+ changed: false,
8338
+ message: "Not a git repository or git is unavailable.",
8339
+ errors: ["Not a git repository or git is unavailable."],
8340
+ warnings: [],
8341
+ };
8342
+ }
8343
+ const existing = safeReadText(hookPath) ?? "";
8344
+ const installed = hasKageHookBlock(existing);
8345
+ if (!installed) {
8346
+ return {
8347
+ ok: true,
8348
+ action: "uninstall",
8349
+ project_dir: projectDir,
8350
+ hook_path: hookPath,
8351
+ installed: false,
8352
+ changed: false,
8353
+ message: "Kage post-commit hook was not installed.",
8354
+ errors: [],
8355
+ warnings: (0, node_fs_1.existsSync)(hookPath) ? [] : ["No post-commit hook file exists."],
8356
+ };
8357
+ }
8358
+ const next = stripKageHookBlock(existing);
8359
+ (0, node_fs_1.writeFileSync)(hookPath, next, "utf8");
8360
+ (0, node_fs_1.chmodSync)(hookPath, 0o755);
8361
+ return {
8362
+ ok: true,
8363
+ action: "uninstall",
8364
+ project_dir: projectDir,
8365
+ hook_path: hookPath,
8366
+ installed: false,
8367
+ changed: true,
8368
+ message: "Removed Kage post-commit hook.",
8369
+ errors: [],
8370
+ warnings: [],
8371
+ };
8372
+ }
8220
8373
  function exportPublicBundle(projectDir) {
8221
8374
  ensureMemoryDirs(projectDir);
8222
8375
  const candidates = loadPacketsFromDir(publicCandidatesDir(projectDir));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.26",
3
+ "version": "1.1.27",
4
4
  "description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
5
5
  "main": "dist/index.js",
6
6
  "files": [
package/viewer/app.js CHANGED
@@ -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=20">
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=20"></script>
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; }