@plumpslabs/kuma 2.4.17 → 2.4.20

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/index.js CHANGED
@@ -3366,6 +3366,33 @@ async function scanCodebase(options = {}) {
3366
3366
  }
3367
3367
  }
3368
3368
  const db = await getDb();
3369
+ const ensureFileNode = async (relPath) => {
3370
+ const fid = nodeId("file", relPath);
3371
+ try {
3372
+ const chk = db.prepare(`SELECT 1 FROM nodes WHERE id = ? LIMIT 1`);
3373
+ chk.bind([fid]);
3374
+ const exists = chk.step();
3375
+ chk.free();
3376
+ if (!exists) {
3377
+ await upsertNode({ id: fid, type: "file", name: relPath, filePath: relPath, metadata: { backbone: true } });
3378
+ result.nodeCount++;
3379
+ }
3380
+ } catch {
3381
+ }
3382
+ return fid;
3383
+ };
3384
+ const nodeExists = (id) => {
3385
+ try {
3386
+ const chk = db.prepare(`SELECT 1 FROM nodes WHERE id = ? LIMIT 1`);
3387
+ chk.bind([id]);
3388
+ const exists = chk.step();
3389
+ chk.free();
3390
+ return exists;
3391
+ } catch {
3392
+ return false;
3393
+ }
3394
+ };
3395
+ const pendingCalls = [];
3369
3396
  for (const parsed of parsedFiles) {
3370
3397
  const { filePath, symbols, imports, calledSymbols, isTest } = parsed;
3371
3398
  const fileId = nodeId("file", filePath);
@@ -3421,14 +3448,16 @@ async function scanCodebase(options = {}) {
3421
3448
  if (sym.extends) {
3422
3449
  const parentFile = symbolMap.get(sym.extends) || filePath;
3423
3450
  const parentId = `class::${parentFile}::${sym.extends}`;
3424
- await addEdge({ sourceId: symId, targetId: parentId, type: "extends" });
3425
- result.edgeCount++;
3451
+ if (nodeExists(parentId)) {
3452
+ await addEdge({ sourceId: symId, targetId: parentId, type: "extends" });
3453
+ result.edgeCount++;
3454
+ }
3426
3455
  }
3427
3456
  }
3428
3457
  for (const imp of imports) {
3429
3458
  const resolved = resolveImportPath(filePath, imp.source, root);
3430
3459
  if (resolved) {
3431
- const targetFileId = nodeId("file", resolved);
3460
+ const targetFileId = await ensureFileNode(resolved);
3432
3461
  await addEdge({
3433
3462
  sourceId: fileId,
3434
3463
  targetId: targetFileId,
@@ -3449,17 +3478,17 @@ async function scanCodebase(options = {}) {
3449
3478
  for (const callee of calledSymbols) {
3450
3479
  const calleeFile = symbolMap.get(callee);
3451
3480
  if (calleeFile && calleeFile !== filePath) {
3452
- const calleeId = `function::${calleeFile}::${callee}`;
3453
- await addEdge({
3454
- sourceId: fileId,
3455
- targetId: calleeId,
3456
- type: "calls"
3457
- });
3458
- result.edgeCount++;
3481
+ pendingCalls.push({ fileId, callee, calleeFile, filePath });
3459
3482
  }
3460
3483
  }
3461
3484
  result.filesScanned++;
3462
3485
  }
3486
+ for (const p of pendingCalls) {
3487
+ const calleeId = `function::${p.calleeFile}::${p.callee}`;
3488
+ if (!nodeExists(calleeId)) continue;
3489
+ await addEdge({ sourceId: p.fileId, targetId: calleeId, type: "calls" });
3490
+ result.edgeCount++;
3491
+ }
3463
3492
  saveDb(db);
3464
3493
  return result;
3465
3494
  }
@@ -5771,6 +5800,7 @@ __export(kumaGraph_exports, {
5771
5800
  getGraphStats: () => getGraphStats,
5772
5801
  nodeId: () => nodeId,
5773
5802
  propagateImpact: () => propagateImpact,
5803
+ pruneOrphans: () => pruneOrphans,
5774
5804
  queryGraph: () => queryGraph,
5775
5805
  recordAffects: () => recordAffects,
5776
5806
  recordApiRoute: () => recordApiRoute,
@@ -5847,6 +5877,48 @@ async function pruneGraphIfNeeded() {
5847
5877
  return { pruned: false, removedNodes: 0, removedEdges: 0 };
5848
5878
  }
5849
5879
  }
5880
+ async function pruneOrphans(opts = {}) {
5881
+ const olderThanSec = opts.olderThanSec ?? 3600;
5882
+ const dryRun = opts.dryRun ?? false;
5883
+ let orphanNodes = 0;
5884
+ let danglingEdges = 0;
5885
+ try {
5886
+ const db = await getDb();
5887
+ const cutoff = Math.floor(Date.now() / 1e3) - olderThanSec;
5888
+ const dangling = db.exec(`
5889
+ SELECT COUNT(*) FROM edges e
5890
+ WHERE NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = e.target_id)
5891
+ OR NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = e.source_id)
5892
+ `);
5893
+ danglingEdges = Number(dangling[0]?.values?.[0]?.[0]) || 0;
5894
+ if (!dryRun && danglingEdges > 0) {
5895
+ db.exec(`
5896
+ DELETE FROM edges
5897
+ WHERE NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = edges.target_id)
5898
+ OR NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = edges.source_id)
5899
+ `);
5900
+ }
5901
+ const orphans = db.exec(`
5902
+ SELECT COUNT(*) FROM nodes n
5903
+ WHERE n.type IN ('file','function','class','method','interface','test','feature_domain','arch_flow')
5904
+ AND n.updated_at < ${cutoff}
5905
+ AND NOT EXISTS (SELECT 1 FROM edges e WHERE e.source_id = n.id OR e.target_id = n.id)
5906
+ `);
5907
+ orphanNodes = Number(orphans[0]?.values?.[0]?.[0]) || 0;
5908
+ if (!dryRun && orphanNodes > 0) {
5909
+ db.exec(`
5910
+ DELETE FROM nodes
5911
+ WHERE type IN ('file','function','class','method','interface','test','feature_domain','arch_flow')
5912
+ AND updated_at < ${cutoff}
5913
+ AND id NOT IN (SELECT source_id FROM edges UNION SELECT target_id FROM edges)
5914
+ `);
5915
+ }
5916
+ if (!dryRun && (orphanNodes > 0 || danglingEdges > 0)) saveDb(db);
5917
+ } catch (err) {
5918
+ console.error(`[KumaGraph] pruneOrphans failed: ${err}`);
5919
+ }
5920
+ return { orphanNodes, danglingEdges };
5921
+ }
5850
5922
  async function upsertNode(node) {
5851
5923
  try {
5852
5924
  const db = await getDb();
@@ -5961,6 +6033,9 @@ async function recordApiRoute(route, handler) {
5961
6033
  async function recordDomainFlow(params) {
5962
6034
  let nodeCount = 0;
5963
6035
  let edgeCount = 0;
6036
+ if (!params.hops || params.hops.length === 0) {
6037
+ return { nodeCount, edgeCount };
6038
+ }
5964
6039
  try {
5965
6040
  const domainId = nodeId("feature_domain", params.domain);
5966
6041
  let fileHashes = {};
@@ -11397,6 +11472,30 @@ function opencodePluginTemplate() {
11397
11472
  " },",
11398
11473
  " };",
11399
11474
  "};",
11475
+ "",
11476
+ "// Server-runtime definition (required by newer OpenCode server).",
11477
+ "// Reuses the same hooks via the named export above.",
11478
+ "const kumaHooks = {",
11479
+ ' "tool.execute.before": async (input, output) => {',
11480
+ " const hooks = await KumaPlugin();",
11481
+ ' return hooks["tool.execute.before"](input, output);',
11482
+ " },",
11483
+ ' "file.edited": async (input) => {',
11484
+ " const hooks = await KumaPlugin();",
11485
+ ' return hooks["file.edited"](input);',
11486
+ " },",
11487
+ ' "experimental.session.compacting": async (input, output) => {',
11488
+ " const hooks = await KumaPlugin();",
11489
+ ' return hooks["experimental.session.compacting"](input, output);',
11490
+ " },",
11491
+ "};",
11492
+ "",
11493
+ "export default {",
11494
+ ' id: "kuma",',
11495
+ " // NOTE: setup must return hooks synchronously \u2014 never an async",
11496
+ " // effect/event-loop. An async effect never resolves and hangs load.",
11497
+ " setup: () => kumaHooks,",
11498
+ "};",
11400
11499
  ""
11401
11500
  ].join("\n");
11402
11501
  }
@@ -12935,6 +13034,14 @@ async function daemonTick(root) {
12935
13034
  notes.push(`drift-report: ${items} item(s) \u2192 .kuma/drift.json`);
12936
13035
  } catch {
12937
13036
  }
13037
+ try {
13038
+ const { pruneOrphans: pruneOrphans2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
13039
+ const { orphanNodes, danglingEdges } = await pruneOrphans2({ olderThanSec: 3600 });
13040
+ if (orphanNodes > 0 || danglingEdges > 0) {
13041
+ notes.push(`orphans: -${orphanNodes}n/-${danglingEdges}e pruned`);
13042
+ }
13043
+ } catch {
13044
+ }
12938
13045
  try {
12939
13046
  const { clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
12940
13047
  clearDirty2();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.4.17",
3
+ "version": "2.4.20",
4
4
  "description": "Safety-first context & orchestration engine for AI coding agents. MCP server with mandatory research pipeline, knowledge graph, impact analysis, decision memory, and safety guard — works with any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
3
  "name": "kuma",
4
- "version": "2.4.17",
4
+ "version": "2.4.20",
5
5
  "description": "Safety-first context & orchestration engine for AI coding agents. Shadow memory, gotcha lifecycle, blast radius impact analysis, and test verification.",
6
6
  "author": {
7
7
  "name": "plumpslabs",