@kage-core/kage-graph-mcp 1.2.0 → 1.3.0

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/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ function usage() {
12
12
 
13
13
  Usage:
14
14
  kage index --project <dir>
15
+ kage demo [--project <dir>]
15
16
  kage init --project <dir>
16
17
  kage policy --project <dir>
17
18
  kage doctor --project <dir>
@@ -31,6 +32,7 @@ Usage:
31
32
  kage gc --project <dir> [--dry-run] [--force] [--json]
32
33
  kage compact --project <dir> [--dry-run] [--json]
33
34
  kage verify --project <dir> [--id <packet-id>] [--json]
35
+ kage suppressed --project <dir> [--json]
34
36
  kage pr summarize --project <dir> [--json]
35
37
  kage pr check --project <dir> [--json]
36
38
  kage upgrade [--dry-run]
@@ -60,6 +62,7 @@ Usage:
60
62
  kage inbox --project <dir> [--json]
61
63
  kage quality --project <dir> [--json]
62
64
  kage benchmark --project <dir> [--json]
65
+ kage benchmark --trust --project <dir> [--json]
63
66
  kage benchmark --memory-quality [--json]
64
67
  kage benchmark --scale [--sizes 240,1000,5000] [--json]
65
68
  kage benchmark --project <dir> --compare --task <task> [--json]
@@ -176,6 +179,30 @@ async function main() {
176
179
  console.log(`Indexes:\n${result.indexes.map((path) => ` - ${path}`).join("\n")}`);
177
180
  return;
178
181
  }
182
+ if (command === "demo") {
183
+ const demoDir = takeArg(args, "--project") ?? `${process.cwd()}/kage-demo`;
184
+ const result = (0, kernel_js_1.runDemo)(demoDir);
185
+ if (args.includes("--json")) {
186
+ console.log(JSON.stringify(result, null, 2));
187
+ return;
188
+ }
189
+ console.log("Kage demo — can you trust your agent's memory?\n");
190
+ console.log(`Seeded ${result.captured.length} grounded memories in ${result.project_dir}\n`);
191
+ console.log("1. Hallucinated citation — REJECTED on write:");
192
+ if (result.rejected_hallucination) {
193
+ console.log(` ✗ "${result.rejected_hallucination.title}"`);
194
+ console.log(` ${result.rejected_hallucination.error}\n`);
195
+ }
196
+ console.log("2. Stale memory (cited file deleted) — WITHHELD from recall:");
197
+ for (const w of result.withheld)
198
+ console.log(` ⊘ ${w.title}\n ${w.reason}`);
199
+ console.log("\n3. Recall returns only grounded, current memory:");
200
+ for (const t of result.recalled)
201
+ console.log(` ✓ ${t}`);
202
+ console.log(`\nTrust score: ${result.trust_score}/100`);
203
+ console.log(`\nSee it in the viewer: ${result.viewer_command}`);
204
+ return;
205
+ }
179
206
  if (command === "init") {
180
207
  const result = (0, kernel_js_1.initProject)(projectArg(args));
181
208
  console.log(`Initialized Kage memory for ${result.index.projectDir}`);
@@ -465,6 +492,21 @@ async function main() {
465
492
  }
466
493
  return;
467
494
  }
495
+ if (command === "suppressed") {
496
+ const result = (0, kernel_js_1.kageSuppressedMemory)(projectArg(args));
497
+ if (args.includes("--json")) {
498
+ console.log(JSON.stringify(result, null, 2));
499
+ return;
500
+ }
501
+ console.log(`Kage suppressed memory — ${result.count} packet(s) currently withheld from recall`);
502
+ for (const item of result.items) {
503
+ console.log(` ⊘ ${item.title}`);
504
+ console.log(` ${item.reason}`);
505
+ }
506
+ if (!result.count)
507
+ console.log(" (none — all recallable memory is grounded and current)");
508
+ return;
509
+ }
468
510
  if (command === "refresh") {
469
511
  const result = (0, kernel_js_1.refreshProject)(projectArg(args), { full: args.includes("--full") });
470
512
  if (args.includes("--json")) {
@@ -1319,6 +1361,24 @@ async function main() {
1319
1361
  return;
1320
1362
  }
1321
1363
  if (command === "benchmark") {
1364
+ if (args.includes("--trust")) {
1365
+ const result = (0, kernel_js_1.benchmarkTrust)(projectArg(args));
1366
+ if (args.includes("--json")) {
1367
+ console.log(JSON.stringify(result, null, 2));
1368
+ if (!result.ok)
1369
+ process.exitCode = 1;
1370
+ return;
1371
+ }
1372
+ console.log("Kage Trust Benchmark — can this memory be trusted?");
1373
+ console.log(`Trust score: ${result.trust_score}/100 (${result.ok ? "PASS" : "REVIEW"})`);
1374
+ console.log(` Hallucinated-citation rejection: ${result.metrics.hallucinated_citation_rejection_rate}% (${result.detail.hallucination.rejected}/${result.detail.hallucination.attempted})`);
1375
+ console.log(` Stale-memory exclusion: ${result.metrics.stale_memory_exclusion_rate}% (${result.detail.staleness.excluded_after}/${result.detail.staleness.recallable_before})`);
1376
+ console.log(` Live grounding rate: ${result.metrics.live_grounding_rate}% (${result.detail.live_memory.grounded}/${result.detail.live_memory.checked} packets)`);
1377
+ console.log(` Wrong-advice prevented: ${result.metrics.wrong_advice_prevented_rate}%`);
1378
+ if (!result.ok)
1379
+ process.exitCode = 1;
1380
+ return;
1381
+ }
1322
1382
  if (args.includes("--memory-quality")) {
1323
1383
  const result = (0, kernel_js_1.benchmarkCodingMemoryQuality)({
1324
1384
  topK: Number(takeArg(args, "--top-k") ?? 10),
package/dist/daemon.js CHANGED
@@ -573,6 +573,8 @@ async function startViewer(projectDir, options = {}) {
573
573
  const timelinePath = (0, node_path_1.join)(reportsDir, "timeline.json");
574
574
  const lineagePath = (0, node_path_1.join)(reportsDir, "lineage.json");
575
575
  const setupPath = (0, node_path_1.join)(reportsDir, "setup.json");
576
+ const trustPath = (0, node_path_1.join)(reportsDir, "trust.json");
577
+ const suppressedPath = (0, node_path_1.join)(reportsDir, "suppressed.json");
576
578
  // Pre-generate lightweight JSON reports so the viewer can load them directly.
577
579
  try {
578
580
  (0, node_fs_1.mkdirSync)(reportsDir, { recursive: true });
@@ -601,11 +603,13 @@ async function startViewer(projectDir, options = {}) {
601
603
  (0, node_fs_1.writeFileSync)(timelinePath, JSON.stringify((0, kernel_js_1.kageMemoryTimeline)(projectDir), null, 2));
602
604
  (0, node_fs_1.writeFileSync)(lineagePath, JSON.stringify((0, kernel_js_1.kageMemoryLineage)(projectDir), null, 2));
603
605
  (0, node_fs_1.writeFileSync)(setupPath, JSON.stringify((0, kernel_js_1.setupDoctor)(projectDir), null, 2));
606
+ (0, node_fs_1.writeFileSync)(trustPath, JSON.stringify((0, kernel_js_1.benchmarkTrust)(projectDir), null, 2));
607
+ (0, node_fs_1.writeFileSync)(suppressedPath, JSON.stringify((0, kernel_js_1.kageSuppressedMemory)(projectDir), null, 2));
604
608
  }
605
609
  catch {
606
610
  // non-fatal: viewer will show 404 for reports if generation fails
607
611
  }
608
- const url = `http://${host}:${port}/viewer/index.html?graph=${encodeURIComponent(graphPath)}&code=${encodeURIComponent(codePath)}&metrics=${encodeURIComponent(metricsPath)}&inbox=${encodeURIComponent(inboxPath)}&review=${encodeURIComponent(reviewPath)}&pending=${encodeURIComponent(pendingDir)}&quality=${encodeURIComponent(qualityPath)}&benchmark=${encodeURIComponent(benchmarkPath)}&contributors=${encodeURIComponent(contributorsPath)}&profile=${encodeURIComponent(profilePath)}&xray=${encodeURIComponent(xrayPath)}&capabilities=${encodeURIComponent(capabilitiesPath)}&slots=${encodeURIComponent(slotsPath)}&decisions=${encodeURIComponent(decisionsPath)}&risk=${encodeURIComponent(riskPath)}&moduleHealth=${encodeURIComponent(moduleHealthPath)}&graphInsights=${encodeURIComponent(graphInsightsPath)}&workspace=${encodeURIComponent(workspacePath)}&sessions=${encodeURIComponent(sessionsPath)}&replay=${encodeURIComponent(replayPath)}&memoryAccess=${encodeURIComponent(memoryAccessPath)}&memoryAudit=${encodeURIComponent(memoryAuditPath)}&handoff=${encodeURIComponent(handoffPath)}&lifecycle=${encodeURIComponent(lifecyclePath)}&timeline=${encodeURIComponent(timelinePath)}&lineage=${encodeURIComponent(lineagePath)}&setup=${encodeURIComponent(setupPath)}&view=code`;
612
+ const url = `http://${host}:${port}/viewer/index.html?graph=${encodeURIComponent(graphPath)}&code=${encodeURIComponent(codePath)}&metrics=${encodeURIComponent(metricsPath)}&inbox=${encodeURIComponent(inboxPath)}&review=${encodeURIComponent(reviewPath)}&pending=${encodeURIComponent(pendingDir)}&quality=${encodeURIComponent(qualityPath)}&benchmark=${encodeURIComponent(benchmarkPath)}&contributors=${encodeURIComponent(contributorsPath)}&profile=${encodeURIComponent(profilePath)}&xray=${encodeURIComponent(xrayPath)}&capabilities=${encodeURIComponent(capabilitiesPath)}&slots=${encodeURIComponent(slotsPath)}&decisions=${encodeURIComponent(decisionsPath)}&risk=${encodeURIComponent(riskPath)}&moduleHealth=${encodeURIComponent(moduleHealthPath)}&graphInsights=${encodeURIComponent(graphInsightsPath)}&workspace=${encodeURIComponent(workspacePath)}&sessions=${encodeURIComponent(sessionsPath)}&replay=${encodeURIComponent(replayPath)}&memoryAccess=${encodeURIComponent(memoryAccessPath)}&memoryAudit=${encodeURIComponent(memoryAuditPath)}&handoff=${encodeURIComponent(handoffPath)}&lifecycle=${encodeURIComponent(lifecyclePath)}&timeline=${encodeURIComponent(timelinePath)}&lineage=${encodeURIComponent(lineagePath)}&setup=${encodeURIComponent(setupPath)}&trust=${encodeURIComponent(trustPath)}&suppressed=${encodeURIComponent(suppressedPath)}&view=code`;
609
613
  const server = (0, node_http_1.createServer)((req, res) => {
610
614
  const requestUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
611
615
  let filePath = null;
package/dist/index.js CHANGED
@@ -590,7 +590,7 @@ function listTools() {
590
590
  type: "object",
591
591
  properties: {
592
592
  project_dir: { type: "string" },
593
- mode: { type: "string", enum: ["project", "memory_quality", "memory_scale"] },
593
+ mode: { type: "string", enum: ["project", "trust", "memory_quality", "memory_scale"] },
594
594
  sizes: { type: "array", items: { type: "number" } },
595
595
  top_k: { type: "number" },
596
596
  },
@@ -1376,14 +1376,16 @@ async function callTool(name, args) {
1376
1376
  }
1377
1377
  if (name === "kage_benchmark") {
1378
1378
  const mode = String(args?.mode ?? "project");
1379
- const result = mode === "memory_quality"
1380
- ? (0, kernel_js_1.benchmarkCodingMemoryQuality)({ topK: Number(args?.top_k ?? 10) })
1381
- : mode === "memory_scale"
1382
- ? (0, kernel_js_1.benchmarkMemoryScale)({
1383
- sizes: Array.isArray(args?.sizes) ? args.sizes.map(Number) : undefined,
1384
- topK: Number(args?.top_k ?? 10),
1385
- })
1386
- : (0, kernel_js_1.benchmarkProject)(String(args?.project_dir ?? ""));
1379
+ const result = mode === "trust"
1380
+ ? (0, kernel_js_1.benchmarkTrust)(String(args?.project_dir ?? ""))
1381
+ : mode === "memory_quality"
1382
+ ? (0, kernel_js_1.benchmarkCodingMemoryQuality)({ topK: Number(args?.top_k ?? 10) })
1383
+ : mode === "memory_scale"
1384
+ ? (0, kernel_js_1.benchmarkMemoryScale)({
1385
+ sizes: Array.isArray(args?.sizes) ? args.sizes.map(Number) : undefined,
1386
+ topK: Number(args?.top_k ?? 10),
1387
+ })
1388
+ : (0, kernel_js_1.benchmarkProject)(String(args?.project_dir ?? ""));
1387
1389
  return {
1388
1390
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1389
1391
  };
@@ -1692,5 +1694,16 @@ async function main() {
1692
1694
  await server.connect(transport);
1693
1695
  }
1694
1696
  if (require.main === module) {
1697
+ const firstArg = process.argv[2];
1698
+ if (firstArg && !firstArg.startsWith("-")) {
1699
+ // A positional subcommand (demo, init, recall, ...) means the user wants the
1700
+ // kage CLI, not the MCP stdio server. Delegate so a single
1701
+ // `npx @kage-core/kage-graph-mcp <command>` works like the `kage` binary.
1702
+ // MCP clients launch with no args (or flags), which falls through to the server.
1703
+ const { spawnSync } = require("node:child_process");
1704
+ const { join } = require("node:path");
1705
+ const result = spawnSync(process.execPath, [join(__dirname, "cli.js"), ...process.argv.slice(2)], { stdio: "inherit" });
1706
+ process.exit(result.status ?? 0);
1707
+ }
1695
1708
  main().catch(console.error);
1696
1709
  }
package/dist/kernel.js CHANGED
@@ -83,6 +83,7 @@ exports.buildIndexes = buildIndexes;
83
83
  exports.indexProject = indexProject;
84
84
  exports.refreshProject = refreshProject;
85
85
  exports.gcProject = gcProject;
86
+ exports.kageSuppressedMemory = kageSuppressedMemory;
86
87
  exports.verifyCitations = verifyCitations;
87
88
  exports.compactProject = compactProject;
88
89
  exports.installAgentPolicy = installAgentPolicy;
@@ -114,6 +115,8 @@ exports.kageMetrics = kageMetrics;
114
115
  exports.auditProject = auditProject;
115
116
  exports.memoryInbox = memoryInbox;
116
117
  exports.qualityReport = qualityReport;
118
+ exports.benchmarkTrust = benchmarkTrust;
119
+ exports.runDemo = runDemo;
117
120
  exports.benchmarkProject = benchmarkProject;
118
121
  exports.benchmarkCodingMemoryQuality = benchmarkCodingMemoryQuality;
119
122
  exports.benchmarkMemoryScale = benchmarkMemoryScale;
@@ -5595,8 +5598,20 @@ function gcProject(projectDir, options = {}) {
5595
5598
  total_scanned: packetEntries.length,
5596
5599
  };
5597
5600
  }
5598
- // On-demand citation/freshness check the agent can call before trusting a memory.
5599
- // Pass an id to verify one packet, or omit to audit all approved memory.
5601
+ // The memory recall is actively WITHHOLDING from agents right now (hard-stale:
5602
+ // cited files deleted, ttl expired, or reported stale). This is the human-facing
5603
+ // counterpart to the silent recall-time exclusion — surfaced, never hidden.
5604
+ function kageSuppressedMemory(projectDir) {
5605
+ ensureMemoryDirs(projectDir);
5606
+ const cache = new Map();
5607
+ const items = loadApprovedPackets(projectDir)
5608
+ .map((packet) => {
5609
+ const reason = recallHardStaleReason(projectDir, packet, cache);
5610
+ return reason ? { id: packet.id, title: packet.title, type: packet.type, reason, paths: packet.paths } : null;
5611
+ })
5612
+ .filter((entry) => entry !== null);
5613
+ return { schema_version: 1, generated_at: nowIso(), count: items.length, items };
5614
+ }
5600
5615
  function verifyCitations(projectDir, options = {}) {
5601
5616
  ensureMemoryDirs(projectDir);
5602
5617
  const approved = loadApprovedPackets(projectDir);
@@ -9838,6 +9853,152 @@ function qualityReport(projectDir) {
9838
9853
  packets: rows,
9839
9854
  };
9840
9855
  }
9856
+ // The Trust Benchmark measures what retrieval benchmarks cannot: whether the memory
9857
+ // system can be TRUSTED — does it refuse to store hallucinated citations, does it
9858
+ // withhold memory whose evidence was deleted, and is live repo memory actually grounded.
9859
+ // Controlled gates run in an isolated sandbox; the grounding gate runs on the real repo.
9860
+ function benchmarkTrust(projectDir) {
9861
+ const runDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-trust-"));
9862
+ const sandbox = (0, node_path_1.join)(runDir, "project");
9863
+ try {
9864
+ ensureMemoryDirs(sandbox);
9865
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(sandbox, "src"), { recursive: true });
9866
+ // Gate 1 — Hallucinated-citation rejection: a strict capture whose every cited path
9867
+ // is missing must be rejected. (No competitor validates citations at write time.)
9868
+ const hallucinationAttempts = 8;
9869
+ let rejected = 0;
9870
+ for (let i = 0; i < hallucinationAttempts; i += 1) {
9871
+ const result = capture({
9872
+ projectDir: sandbox,
9873
+ title: `Hallucinated rule ${i}`,
9874
+ body: `Use the helper in src/ghost-${i}.ts for retry handling.`,
9875
+ type: "decision",
9876
+ paths: [`src/ghost-${i}.ts`],
9877
+ strictCitations: true,
9878
+ });
9879
+ if (!result.ok)
9880
+ rejected += 1;
9881
+ }
9882
+ // Gate 2 — Stale-memory exclusion: memory grounded in real files at capture time
9883
+ // must be withheld from recall once those files are deleted (the "deleted since
9884
+ // capture" signal). We only count memories that were recallable BEFORE deletion.
9885
+ const staleAttempts = 8;
9886
+ const recallableBefore = [];
9887
+ for (let i = 0; i < staleAttempts; i += 1) {
9888
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(sandbox, "src", `widget-${i}.ts`), `export const widget${i} = ${i};\n`, "utf8");
9889
+ capture({
9890
+ projectDir: sandbox,
9891
+ title: `Widget ${i} retry invariant`,
9892
+ body: `Widget ${i} retries use idempotency token zeta${i} in src/widget-${i}.ts to avoid duplicate charges.`,
9893
+ type: "decision",
9894
+ paths: [`src/widget-${i}.ts`],
9895
+ });
9896
+ }
9897
+ for (let i = 0; i < staleAttempts; i += 1) {
9898
+ const before = recall(sandbox, `widget ${i} retry idempotency token zeta${i}`, 5, false, { trackAccess: false });
9899
+ recallableBefore[i] = before.results.some((entry) => entry.packet.title === `Widget ${i} retry invariant`);
9900
+ }
9901
+ for (let i = 0; i < staleAttempts; i += 1) {
9902
+ (0, node_fs_1.rmSync)((0, node_path_1.join)(sandbox, "src", `widget-${i}.ts`), { force: true });
9903
+ }
9904
+ let recallableCount = 0;
9905
+ let excludedAfter = 0;
9906
+ for (let i = 0; i < staleAttempts; i += 1) {
9907
+ if (!recallableBefore[i])
9908
+ continue;
9909
+ recallableCount += 1;
9910
+ const after = recall(sandbox, `widget ${i} retry idempotency token zeta${i}`, 5, false, { trackAccess: false });
9911
+ const surfaced = after.results.some((entry) => entry.packet.title === `Widget ${i} retry invariant`);
9912
+ if (!surfaced)
9913
+ excludedAfter += 1;
9914
+ }
9915
+ // Gate 3 — Live grounding: how much of the real repo's approved memory is grounded
9916
+ // (cited files exist) and not stale.
9917
+ const verify = verifyCitations(projectDir);
9918
+ const liveChecked = verify.checked;
9919
+ const grounded = verify.packets.filter((entry) => entry.grounded && !entry.stale).length;
9920
+ const hallucinationRate = percent(rejected, hallucinationAttempts);
9921
+ const staleRate = percent(excludedAfter, recallableCount || staleAttempts);
9922
+ const liveGroundingRate = liveChecked > 0 ? percent(grounded, liveChecked) : 100;
9923
+ const wrongAdvicePrevented = percent(rejected + excludedAfter, hallucinationAttempts + (recallableCount || staleAttempts));
9924
+ const gates = [
9925
+ { name: "hallucinated_citation_rejection", target: 100, actual: hallucinationRate, unit: "percent", pass: hallucinationRate >= 100 },
9926
+ { name: "stale_memory_exclusion", target: 100, actual: staleRate, unit: "percent", pass: staleRate >= 100 },
9927
+ { name: "live_grounding_rate", target: 80, actual: liveGroundingRate, unit: "percent", pass: liveGroundingRate >= 80 },
9928
+ ];
9929
+ const trustScore = Math.round((hallucinationRate + staleRate + liveGroundingRate) / 3);
9930
+ return {
9931
+ schema_version: 1,
9932
+ generated_at: nowIso(),
9933
+ ok: gates.every((gate) => gate.pass),
9934
+ trust_score: trustScore,
9935
+ gates,
9936
+ metrics: {
9937
+ hallucinated_citation_rejection_rate: hallucinationRate,
9938
+ stale_memory_exclusion_rate: staleRate,
9939
+ live_grounding_rate: liveGroundingRate,
9940
+ wrong_advice_prevented_rate: wrongAdvicePrevented,
9941
+ },
9942
+ detail: {
9943
+ hallucination: { attempted: hallucinationAttempts, rejected },
9944
+ staleness: { recallable_before: recallableCount, excluded_after: excludedAfter },
9945
+ live_memory: { checked: liveChecked, grounded, stale: verify.stale },
9946
+ },
9947
+ };
9948
+ }
9949
+ finally {
9950
+ (0, node_fs_1.rmSync)(runDir, { recursive: true, force: true });
9951
+ }
9952
+ }
9953
+ // `kage demo`: a self-contained 60-second proof of the trust wedge. Seeds a tiny
9954
+ // repo with grounded memory, then shows Kage (1) reject a hallucinated citation,
9955
+ // (2) withhold a memory whose cited file was deleted, and (3) recall only grounded
9956
+ // memory — the three things that make agent memory trustworthy.
9957
+ function runDemo(demoDir) {
9958
+ (0, node_fs_1.rmSync)(demoDir, { recursive: true, force: true });
9959
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(demoDir, "src"), { recursive: true });
9960
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(demoDir, "src", "auth.ts"), "export function validateToken() { return true; }\n", "utf8");
9961
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(demoDir, "src", "payments.ts"), "export function charge() { return 'ok'; }\n", "utf8");
9962
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(demoDir, "src", "legacy-retry.ts"), "export function retry() { return 1; }\n", "utf8");
9963
+ ensureMemoryDirs(demoDir);
9964
+ const captured = [];
9965
+ for (const m of [
9966
+ { title: "Auth uses jose, not jsonwebtoken", body: "Validate tokens with jose in src/auth.ts; jsonwebtoken was removed.", paths: ["src/auth.ts"] },
9967
+ { title: "Payments must be idempotent", body: "charge() in src/payments.ts must be idempotent to avoid double charges.", paths: ["src/payments.ts"] },
9968
+ { title: "Legacy retry helper is the fallback", body: "Old retry logic lives in src/legacy-retry.ts and is used as a fallback.", paths: ["src/legacy-retry.ts"] },
9969
+ ]) {
9970
+ const r = capture({ projectDir: demoDir, title: m.title, body: m.body, type: "decision", paths: m.paths });
9971
+ if (r.ok && r.packet)
9972
+ captured.push(r.packet.title);
9973
+ }
9974
+ // (2) delete a cited file → that memory becomes stale and is withheld from recall.
9975
+ (0, node_fs_1.unlinkSync)((0, node_path_1.join)(demoDir, "src", "legacy-retry.ts"));
9976
+ // (1) a hallucinated citation is rejected at write time.
9977
+ const hallucinated = capture({
9978
+ projectDir: demoDir,
9979
+ title: "Use the helper in src/ghost.ts",
9980
+ body: "Retry handling lives in src/ghost.ts.",
9981
+ type: "decision",
9982
+ paths: ["src/ghost.ts"],
9983
+ strictCitations: true,
9984
+ });
9985
+ const rejected = hallucinated.ok ? null : { title: "Use the helper in src/ghost.ts", error: hallucinated.errors[0] ?? "rejected" };
9986
+ // (3) recall surfaces grounded memory; the stale one is withheld.
9987
+ const recall = recallWithVectorScores(demoDir, "auth token payments retry idempotency", 5, false, { trackAccess: false });
9988
+ const recalled = recall.results.map((entry) => entry.packet.title);
9989
+ const withheld = kageSuppressedMemory(demoDir).items.map((item) => ({ title: item.title, reason: item.reason }));
9990
+ const trust = benchmarkTrust(demoDir);
9991
+ return {
9992
+ ok: true,
9993
+ project_dir: demoDir,
9994
+ captured,
9995
+ rejected_hallucination: rejected,
9996
+ recalled,
9997
+ withheld,
9998
+ trust_score: trust.trust_score,
9999
+ viewer_command: `kage viewer --project ${demoDir}`,
10000
+ };
10001
+ }
9841
10002
  function benchmarkProject(projectDir, inputs = {}) {
9842
10003
  ensureMemoryDirs(projectDir);
9843
10004
  const built = inputs.codeGraph && inputs.knowledgeGraph ? null : currentOrBuildGraphs(projectDir);
@@ -11150,6 +11311,10 @@ elif event_name == "PreCompact":
11150
11311
  payload = {"type": "session_end", "summary": "Claude Code is compacting context; distill durable observations before compaction."}
11151
11312
  elif event_name == "SessionEnd":
11152
11313
  payload = {"type": "session_end", "summary": "Claude Code session ended; distill durable observations for teammate handoff."}
11314
+ elif event_name == "SubagentStop":
11315
+ payload = {"type": "session_end", "summary": "Subagent finished; distill durable observations from the subagent run."}
11316
+ elif event_name == "PreToolUse":
11317
+ payload = {"type": "tool_use", "tool": tool, "path": path, "command": command, "summary": "About to run: " + compact(command or tool or d, 200)}
11153
11318
  else:
11154
11319
  payload = {"type": "tool_use", "tool": tool, "path": path, "command": command, "summary": compact(d, 320), "text": compact(d)}
11155
11320
 
@@ -11161,7 +11326,7 @@ if [[ -n "$OBSERVATION" ]]; then
11161
11326
  kage observe --project "$CWD" --event "$OBSERVATION" --json >/dev/null 2>&1 || true
11162
11327
  fi
11163
11328
 
11164
- if [[ "$EVENT" == "PreCompact" || "$EVENT" == "SessionEnd" ]]; then
11329
+ if [[ "$EVENT" == "PreCompact" || "$EVENT" == "SessionEnd" || "$EVENT" == "SubagentStop" ]]; then
11165
11330
  kage distill --project "$CWD" --session "$SESSION" --json >/dev/null 2>&1 || true
11166
11331
  fi
11167
11332
 
@@ -11197,11 +11362,13 @@ exit 0
11197
11362
  hooks: {
11198
11363
  SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/session-start.sh", timeout: 5 }] }],
11199
11364
  UserPromptSubmit: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 12 }] }],
11365
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
11200
11366
  PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
11201
11367
  PostToolUseFailure: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
11202
11368
  PreCompact: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
11203
11369
  Stop: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/stop.sh", timeout: 20 }] }],
11204
11370
  SessionEnd: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
11371
+ SubagentStop: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
11205
11372
  },
11206
11373
  };
11207
11374
  setSnippet(path, JSON.stringify({ mcpServers: { kage: server } }, null, 2), [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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
@@ -47,7 +47,9 @@
47
47
  lifecycle: null,
48
48
  timeline: null,
49
49
  lineage: null,
50
- setup: null
50
+ setup: null,
51
+ trust: null,
52
+ suppressed: null
51
53
  },
52
54
  pendingPackets: [],
53
55
  reviewText: "",
@@ -183,6 +185,8 @@
183
185
  edgeCount: document.getElementById("edgeCount"),
184
186
  reviewCount: document.getElementById("reviewCount"),
185
187
  dashboardStats: document.getElementById("dashboardStats"),
188
+ trustHero: document.getElementById("trustHero"),
189
+ suppressionShelf: document.getElementById("suppressionShelf"),
186
190
  repoXray: document.getElementById("repoXray"),
187
191
  repoXrayStatus: document.getElementById("repoXrayStatus"),
188
192
  repoXrayScript: document.getElementById("repoXrayScript"),
@@ -233,8 +237,8 @@
233
237
  var PAGE_META = {
234
238
  overview: {
235
239
  eyebrow: "kage://overview",
236
- title: "Repo dashboard",
237
- summary: "What is safe to change next, what needs attention, and what is ready to hand off."
240
+ title: "Repository overview",
241
+ summary: "Whether this repo's agent memory can be trusted, what needs review, and what's ready to hand off."
238
242
  },
239
243
  graph: {
240
244
  eyebrow: "kage://graph",
@@ -622,6 +626,8 @@
622
626
  var timelinePath = params.get("timeline") || params.get("memoryTimeline") || params.get("memory-timeline");
623
627
  var lineagePath = params.get("lineage") || params.get("memoryLineage") || params.get("memory-lineage");
624
628
  var setupPath = params.get("setup") || params.get("setupDoctor") || params.get("setup-doctor");
629
+ var trustPath = params.get("trust") || params.get("trustBenchmark") || params.get("trust-benchmark");
630
+ var suppressedPath = params.get("suppressed") || params.get("suppressedMemory") || params.get("suppressed-memory");
625
631
  var inferredRoot = inferMemoryRoot(graphPaths[0] || "");
626
632
  if (!inboxPath && inferredRoot) inboxPath = inferredRoot + "/inbox.json";
627
633
  if (!reviewPath && inferredRoot) reviewPath = inferredRoot + "/review/memory-review.md";
@@ -648,6 +654,8 @@
648
654
  if (!timelinePath) timelinePath = inferredRoot + "/reports/timeline.json";
649
655
  if (!lineagePath) lineagePath = inferredRoot + "/reports/lineage.json";
650
656
  if (!setupPath) setupPath = inferredRoot + "/reports/setup.json";
657
+ if (!trustPath) trustPath = inferredRoot + "/reports/trust.json";
658
+ if (!suppressedPath) suppressedPath = inferredRoot + "/reports/suppressed.json";
651
659
  }
652
660
  var jobs = [];
653
661
  if (metricsPath) jobs.push(fetchJson(metricsPath).then(function (metrics) { state.metrics = metrics; }));
@@ -675,6 +683,8 @@
675
683
  if (timelinePath) jobs.push(fetchJson(timelinePath).then(function (report) { state.reports.timeline = report; }).catch(function () { state.reports.timeline = null; }));
676
684
  if (lineagePath) jobs.push(fetchJson(lineagePath).then(function (report) { state.reports.lineage = report; }).catch(function () { state.reports.lineage = null; }));
677
685
  if (setupPath) jobs.push(fetchJson(setupPath).then(function (report) { state.reports.setup = report; }).catch(function () { state.reports.setup = null; }));
686
+ if (trustPath) jobs.push(fetchJson(trustPath).then(function (report) { state.reports.trust = report; }).catch(function () { state.reports.trust = null; }));
687
+ if (suppressedPath) jobs.push(fetchJson(suppressedPath).then(function (report) { state.reports.suppressed = report; }).catch(function () { state.reports.suppressed = null; }));
678
688
  if (!graphPaths.length && !jobs.length) {
679
689
  loadHostedDefault();
680
690
  return;
@@ -723,7 +733,9 @@
723
733
  fetchJson("./data/kage/reports/lifecycle.json").catch(function () { return null; }),
724
734
  fetchJson("./data/kage/reports/timeline.json").catch(function () { return null; }),
725
735
  fetchJson("./data/kage/reports/lineage.json").catch(function () { return null; }),
726
- fetchJson("./data/kage/reports/setup.json").catch(function () { return null; })
736
+ fetchJson("./data/kage/reports/setup.json").catch(function () { return null; }),
737
+ fetchJson("./data/kage/reports/trust.json").catch(function () { return null; }),
738
+ fetchJson("./data/kage/reports/suppressed.json").catch(function () { return null; })
727
739
  ]).then(function (items) {
728
740
  var merged = mergeNormalizedGraphs([normalizeGraph(items[0]), normalizeGraph(items[1])]);
729
741
  state.metrics = items[2];
@@ -747,6 +759,8 @@
747
759
  state.reports.timeline = items[20];
748
760
  state.reports.lineage = items[21];
749
761
  state.reports.setup = items[22];
762
+ state.reports.trust = items[23];
763
+ state.reports.suppressed = items[24];
750
764
  loadNormalizedGraph(merged, "Kage repo graph");
751
765
  setAutoLoad("Kage repo graph loaded", true);
752
766
  }).catch(function () {
@@ -2983,6 +2997,75 @@
2983
2997
  ].forEach(function (card) { els.debugOverview.appendChild(card); });
2984
2998
  }
2985
2999
 
3000
+ function renderTrustHero(trust) {
3001
+ if (!els.trustHero) return;
3002
+ var metrics = (trust && trust.metrics) || {};
3003
+ var score = trust && typeof trust.trust_score === "number" ? trust.trust_score : null;
3004
+ var status = score == null ? "idle" : (score >= 90 ? "ok" : score >= 70 ? "warn" : "alert");
3005
+ var bars = [
3006
+ ["Hallucinated citations rejected", metrics.hallucinated_citation_rejection_rate],
3007
+ ["Stale memory excluded from recall", metrics.stale_memory_exclusion_rate],
3008
+ ["Live memory grounded to code", metrics.live_grounding_rate]
3009
+ ];
3010
+ var note = score == null
3011
+ ? "Run <code>kage benchmark --trust</code> to score this repo."
3012
+ : (status === "ok"
3013
+ ? "Verified — agents recall only memory that is grounded and current."
3014
+ : "Some memory needs review before agents should trust it.");
3015
+ var barsHtml = bars.map(function (bar) {
3016
+ var raw = bar[1];
3017
+ var has = !(raw === null || raw === undefined || isNaN(raw));
3018
+ var pct = has ? Math.max(0, Math.min(100, Number(raw))) : 0;
3019
+ return '<div class="trust-bar">'
3020
+ + '<span class="trust-bar-label"></span>'
3021
+ + '<span class="trust-bar-track"><i style="width:' + pct + '%"></i></span>'
3022
+ + '<b class="trust-bar-value">' + (has ? pct + "%" : "—") + '</b>'
3023
+ + '</div>';
3024
+ }).join("");
3025
+ els.trustHero.setAttribute("data-status", status);
3026
+ els.trustHero.innerHTML =
3027
+ '<div class="trust-hero-score">'
3028
+ + '<span class="trust-hero-eyebrow">Memory Trust</span>'
3029
+ + '<div class="trust-hero-number"><strong>' + (score == null ? "—" : score) + '</strong><span>/100</span></div>'
3030
+ + '<p class="trust-hero-note">' + note + '</p>'
3031
+ + '</div>'
3032
+ + '<div class="trust-hero-bars">' + barsHtml + '</div>';
3033
+ // Set labels via textContent to avoid any HTML injection from labels.
3034
+ var labelEls = els.trustHero.querySelectorAll(".trust-bar-label");
3035
+ for (var i = 0; i < labelEls.length; i += 1) labelEls[i].textContent = bars[i][0];
3036
+ }
3037
+
3038
+ function renderSuppressionShelf(report) {
3039
+ if (!els.suppressionShelf) return;
3040
+ var items = (report && report.items) || [];
3041
+ if (!items.length) {
3042
+ els.suppressionShelf.innerHTML = "";
3043
+ els.suppressionShelf.hidden = true;
3044
+ return;
3045
+ }
3046
+ els.suppressionShelf.hidden = false;
3047
+ var plural = items.length === 1 ? "memory is" : "memories are";
3048
+ els.suppressionShelf.innerHTML =
3049
+ '<div class="suppression-head">'
3050
+ + '<div><span class="suppression-eyebrow">Withheld from recall</span>'
3051
+ + '<h2>' + items.length + ' ' + plural + ' being withheld from your agents</h2></div>'
3052
+ + '<strong class="suppression-flag">Needs review</strong>'
3053
+ + '</div>'
3054
+ + '<p class="suppression-sub">Recall is hiding these because their cited evidence was deleted or expired. Verify, update, or supersede each before agents should trust them.</p>'
3055
+ + '<div class="suppression-list">'
3056
+ + items.slice(0, 8).map(function () {
3057
+ return '<div class="suppression-item"><span class="suppression-item-title"></span><span class="suppression-item-reason"></span></div>';
3058
+ }).join("")
3059
+ + '</div>'
3060
+ + (items.length > 8 ? '<p class="suppression-more">+ ' + (items.length - 8) + ' more — run <code>kage suppressed</code></p>' : '');
3061
+ var titleEls = els.suppressionShelf.querySelectorAll(".suppression-item-title");
3062
+ var reasonEls = els.suppressionShelf.querySelectorAll(".suppression-item-reason");
3063
+ for (var i = 0; i < titleEls.length; i += 1) {
3064
+ titleEls[i].textContent = items[i].title;
3065
+ reasonEls[i].textContent = items[i].reason;
3066
+ }
3067
+ }
3068
+
2986
3069
  function renderDashboard() {
2987
3070
  if (!els.dashboardStats) return;
2988
3071
  var metrics = state.metrics || {};
@@ -3009,6 +3092,8 @@
3009
3092
  var readiness = handoff || dashboardReadiness(metrics, pendingReview, staleFlags, duplicateFlags, missingContext);
3010
3093
  var memoryCoverage = dashboardMemoryCoverage(reports, memoryCodeEdges, memoryGraph, memoryNodes);
3011
3094
  var riskHealth = riskTargets.length || hotspots ? (riskTargets.length + hotspots) + " checks" : "No flags";
3095
+ renderTrustHero(reports.trust);
3096
+ renderSuppressionShelf(reports.suppressed);
3012
3097
  var statRows = [
3013
3098
  ["Handoff", readiness.label, readiness.detail, readiness.status],
3014
3099
  ["Memory", memoryCoverage.label, memoryCoverage.detail, memoryCoverage.status],
@@ -3547,6 +3632,10 @@
3547
3632
  return state.entities.filter(function (entity) { return entity.type === type; }).length;
3548
3633
  }
3549
3634
 
3635
+ function numOrDash(value) {
3636
+ return (value === null || value === undefined || isNaN(value)) ? "—" : value;
3637
+ }
3638
+
3550
3639
  function formatDashboardValue(value) {
3551
3640
  if (typeof value === "number" && Number.isFinite(value)) return value.toLocaleString();
3552
3641
  return String(value == null ? "n/a" : value);
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 viewer</title>
7
- <link rel="stylesheet" href="./styles.css?v=37">
7
+ <link rel="stylesheet" href="./styles.css?v=39">
8
8
  </head>
9
9
  <body>
10
10
  <div class="viewer-shell">
@@ -54,16 +54,18 @@
54
54
 
55
55
  <main class="layout">
56
56
  <section class="dashboard-panel" aria-label="Kage repo dashboard">
57
+ <div id="trustHero" class="trust-hero" aria-label="Memory trust score"></div>
58
+ <div id="suppressionShelf" class="suppression-shelf" aria-label="Memory withheld from recall" hidden></div>
57
59
  <div id="dashboardStats" class="dashboard-stats" aria-label="Repo dashboard stats"></div>
58
- <section id="repoXray" class="repo-xray" aria-label="Repo X-Ray">
60
+ <section id="repoXray" class="repo-xray" aria-label="Repository map">
59
61
  <div class="repo-xray-head">
60
62
  <div>
61
- <span>Repo X-Ray</span>
62
- <h2>Show me what you understand</h2>
63
+ <span>Repository map</span>
64
+ <h2>What Kage understands about this repo</h2>
63
65
  </div>
64
66
  <strong id="repoXrayStatus">waiting</strong>
65
67
  </div>
66
- <p id="repoXrayScript">Kage will map entry points, core files, risk, tests, and memory overlays when the X-Ray report is loaded.</p>
68
+ <p id="repoXrayScript">Entry points, core modules, change risk, tests, and memory overlays mapped from the code graph.</p>
67
69
  <div id="repoXrayLayers" class="repo-xray-layers"></div>
68
70
  </section>
69
71
  <div id="dashboardCharts" class="dashboard-charts" aria-label="Repo health charts"></div>
@@ -302,6 +304,6 @@
302
304
  </section>
303
305
  </div>
304
306
 
305
- <script src="./app.js?v=50"></script>
307
+ <script src="./app.js?v=53"></script>
306
308
  </body>
307
309
  </html>
package/viewer/styles.css CHANGED
@@ -424,6 +424,103 @@ body.viewer-page-data .graph-panel {
424
424
  white-space: nowrap;
425
425
  }
426
426
 
427
+ .trust-hero {
428
+ display: grid;
429
+ grid-template-columns: minmax(170px, 250px) minmax(0, 1fr);
430
+ gap: 30px;
431
+ align-items: center;
432
+ margin-bottom: 18px;
433
+ padding: 24px 28px;
434
+ border: 1px solid rgba(65, 255, 143, 0.24);
435
+ border-radius: 14px;
436
+ background:
437
+ radial-gradient(circle at 0 0, rgba(65, 255, 143, 0.12), transparent 380px),
438
+ linear-gradient(135deg, rgba(8, 17, 13, 0.96), rgba(10, 16, 22, 0.82));
439
+ box-shadow: var(--shadow);
440
+ }
441
+ .trust-hero[data-status="warn"] { border-color: rgba(255, 209, 102, 0.34); }
442
+ .trust-hero[data-status="alert"] { border-color: rgba(255, 107, 107, 0.38); }
443
+ .trust-hero[data-status="idle"] { border-color: var(--line); }
444
+ .trust-hero-eyebrow {
445
+ display: block;
446
+ color: var(--terminal-dim);
447
+ font-size: 11px;
448
+ font-weight: 800;
449
+ letter-spacing: 0.14em;
450
+ text-transform: uppercase;
451
+ }
452
+ .trust-hero-number { display: flex; align-items: baseline; gap: 5px; margin-top: 8px; }
453
+ .trust-hero-number strong {
454
+ font-size: 60px;
455
+ line-height: 1;
456
+ color: var(--terminal-strong);
457
+ text-shadow: 0 0 24px rgba(65, 255, 143, 0.38);
458
+ }
459
+ .trust-hero[data-status="warn"] .trust-hero-number strong { color: var(--warn); text-shadow: 0 0 24px rgba(255, 209, 102, 0.32); }
460
+ .trust-hero[data-status="alert"] .trust-hero-number strong { color: var(--danger); text-shadow: 0 0 24px rgba(255, 107, 107, 0.32); }
461
+ .trust-hero[data-status="idle"] .trust-hero-number strong { color: var(--muted); text-shadow: none; }
462
+ .trust-hero-number span { font-size: 19px; color: var(--muted); }
463
+ .trust-hero-note { margin: 12px 0 0; color: var(--muted); font-size: 12.5px; line-height: 1.45; max-width: 250px; }
464
+ .trust-hero-note code { color: var(--accent); background: var(--accent-soft); padding: 1px 6px; border-radius: 5px; font-size: 11.5px; }
465
+ .trust-hero-bars { display: grid; gap: 15px; }
466
+ .trust-bar {
467
+ display: grid;
468
+ grid-template-columns: minmax(0, 1fr) 52px;
469
+ grid-template-areas: "label value" "track value";
470
+ align-items: center;
471
+ gap: 4px 14px;
472
+ }
473
+ .trust-bar-label { grid-area: label; color: var(--text); font-size: 13px; }
474
+ .trust-bar-track { grid-area: track; height: 8px; border-radius: 999px; background: rgba(215, 249, 223, 0.07); overflow: hidden; }
475
+ .trust-bar-track i { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--brand), var(--accent)); box-shadow: 0 0 12px rgba(65, 255, 143, 0.3); transition: width 0.5s ease; }
476
+ .trust-bar-value { grid-area: value; text-align: right; color: var(--terminal-strong); font-size: 15px; font-weight: 800; }
477
+ @media (max-width: 860px) {
478
+ .trust-hero { grid-template-columns: 1fr; gap: 20px; }
479
+ .trust-hero-note { max-width: none; }
480
+ }
481
+
482
+ .suppression-shelf {
483
+ margin-bottom: 18px;
484
+ padding: 20px 24px;
485
+ border: 1px solid rgba(255, 209, 102, 0.34);
486
+ border-radius: 14px;
487
+ background:
488
+ radial-gradient(circle at 0 0, rgba(255, 209, 102, 0.08), transparent 360px),
489
+ linear-gradient(135deg, rgba(24, 18, 8, 0.92), rgba(16, 14, 10, 0.82));
490
+ }
491
+ .suppression-shelf[hidden] { display: none; }
492
+ .suppression-head { display: flex; align-items: start; justify-content: space-between; gap: 16px; }
493
+ .suppression-eyebrow { display: block; color: var(--warn); font-size: 11px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }
494
+ .suppression-head h2 { margin-top: 5px; color: var(--text); text-shadow: none; font-size: 17px; }
495
+ .suppression-flag {
496
+ flex: none;
497
+ color: var(--warn);
498
+ font-size: 11px;
499
+ font-weight: 800;
500
+ text-transform: uppercase;
501
+ letter-spacing: 0.08em;
502
+ padding: 5px 10px;
503
+ border: 1px solid rgba(255, 209, 102, 0.4);
504
+ border-radius: 999px;
505
+ background: var(--warn-soft);
506
+ }
507
+ .suppression-sub { margin: 8px 0 14px; color: var(--muted); font-size: 12.5px; line-height: 1.45; max-width: 760px; }
508
+ .suppression-list { display: grid; gap: 8px; }
509
+ .suppression-item {
510
+ display: grid;
511
+ grid-template-columns: minmax(0, 1fr) auto;
512
+ align-items: center;
513
+ gap: 16px;
514
+ padding: 11px 14px;
515
+ border: 1px solid rgba(255, 209, 102, 0.16);
516
+ border-radius: 9px;
517
+ background: rgba(8, 17, 13, 0.5);
518
+ }
519
+ .suppression-item-title { color: var(--text); font-size: 13.5px; font-weight: 650; overflow-wrap: anywhere; }
520
+ .suppression-item-reason { color: var(--warn); font-size: 12px; text-align: right; opacity: 0.92; }
521
+ .suppression-more { margin: 12px 0 0; color: var(--muted); font-size: 12px; }
522
+ .suppression-more code { color: var(--accent); background: var(--accent-soft); padding: 1px 5px; border-radius: 5px; }
523
+
427
524
  .dashboard-stats {
428
525
  display: grid;
429
526
  grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -431,8 +528,8 @@ body.viewer-page-data .graph-panel {
431
528
  margin-bottom: 22px;
432
529
  }
433
530
  .dashboard-stat {
434
- min-height: 142px;
435
- padding: 22px;
531
+ min-height: 120px;
532
+ padding: 20px 22px;
436
533
  border: 1px solid rgba(65, 255, 143, 0.12);
437
534
  border-radius: 10px;
438
535
  background: rgba(8, 17, 13, 0.82);