@plumpslabs/kuma 2.3.24 → 2.3.25

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
@@ -142,6 +142,8 @@ Kuma consolidates everything into **3 coarse-grained tools**. Each action trigge
142
142
  | `arch_flow` | Layer 2 — Architecture flow mapping (Issue #17). |
143
143
  | `gotcha` | Layer 3 — Known gotchas & anti-regression shield (Issue #17/#21). |
144
144
  | `layers` | Show all 3 memory layers summary. |
145
+ | `delete_node` | Delete specific node or record from disk & RAM immediately. |
146
+ | `graph_health` | Monitor knowledge graph health, orphan nodes, and duplicate groups. |
145
147
 
146
148
  ### 🛡️ `kuma_safety` — Safety & Policy
147
149
 
package/dist/index.js CHANGED
@@ -2558,7 +2558,7 @@ async function recordDomainFlow(params) {
2558
2558
  await addEdge({
2559
2559
  sourceId: fromId,
2560
2560
  targetId: toId,
2561
- type: "affects",
2561
+ type: "depends_on",
2562
2562
  weight: 0.8,
2563
2563
  metadata: { reason: "arch_flow", domain: params.domain }
2564
2564
  });
@@ -8196,19 +8196,25 @@ function generateInitMdContent() {
8196
8196
  "- `status` \u2014 severity: `low` | `medium` | `high` | `critical` (default: `medium`)",
8197
8197
  "- `description` \u2014 optional workaround/fix description",
8198
8198
  "",
8199
- "### STEP 7: Record Arch Flow (RECORD INLINE \u2014 When Tracing Flow)",
8199
+ "### STEP 7: Record Arch Flow (RECORD COMPLETE FLOW \u2014 Core Logic Only)",
8200
8200
  "```",
8201
- 'kuma_memory({ action: "arch_flow", content: "domain: <name> | hops: <file1> \u2192 <file2> \u2192 <file3>" })',
8201
+ 'kuma_memory({ action: "arch_flow", content: "domain: <name> | hops: <core_file1> \u2192 <core_file2> \u2192 <core_file3>" })',
8202
8202
  "```",
8203
- "**When:** IMMEDIATELY after tracing EACH hop in a data/control flow. Record INLINE, then continue tracing the next hop.",
8204
- "**Why:** Architecture knowledge dissipates fast. Record each hop before moving to the next file/task.",
8203
+ "**When:** AFTER tracing a COMPLETE architecture flow (not per hop).",
8204
+ "**Why:** Record flow architecture for future sessions without cluttering the graph.",
8205
+ "",
8206
+ "**Rules:**",
8207
+ "1. MAX 5 files per flow \u2014 record core business logic files only.",
8208
+ "2. SKIP UI components (*.tsx, *.jsx), Controllers (*Controller.js), Schemas (*Schema.js), and simple CRUD services.",
8209
+ "3. INCLUDE files with complex business logic, decision points, state mutations, or error handling.",
8210
+ "4. Format hops separated by ` \u2192 ` (e.g. `domain: LeadDistributionFlow | hops: LeadRotationService.js \u2192 ProspectAssignmentService.js \u2192 ActivityLogService.js`).",
8205
8211
  "",
8206
8212
  "**\u26A0\uFE0F CRITICAL FORMAT \u2014 Content MUST use this exact syntax:**",
8207
8213
  "```",
8208
- "domain: <DomainName> | hops: <file1.tsx> \u2192 <file2.js> \u2192 <file3.ts> | gotchas: <optional> | decisions: <optional>",
8214
+ "domain: <DomainName> | hops: <core_file1.js> \u2192 <core_file2.js> \u2192 <core_file3.js> | gotchas: <optional> | decisions: <optional>",
8209
8215
  "```",
8210
8216
  "- `domain:` \u2014 short domain name (e.g. `InviteUserFlow`, `LeadManagementFlow`)",
8211
- "- `hops:` \u2014 file names separated by ` \u2192 ` (NOT newlines, NOT commas)",
8217
+ "- `hops:` \u2014 max 5 core file names separated by ` \u2192 `",
8212
8218
  "- `gotchas:` \u2014 optional comma-separated gotchas",
8213
8219
  "- `decisions:` \u2014 optional comma-separated decisions",
8214
8220
  "",
@@ -10377,6 +10383,11 @@ var MEMORY_ALIASES = {
10377
10383
  "read": "session",
10378
10384
  "fetch": "session",
10379
10385
  "current": "session",
10386
+ // Graph health synonyms
10387
+ "graph_health": "graph_health",
10388
+ "graph-health": "graph_health",
10389
+ "health-check": "graph_health",
10390
+ "graph-status": "graph_health",
10380
10391
  // Decision synonyms
10381
10392
  "decision": "decision",
10382
10393
  "adr": "decision",
@@ -10408,7 +10419,6 @@ var MEMORY_ALIASES = {
10408
10419
  "task": "todo",
10409
10420
  "todos": "todo",
10410
10421
  "tasks": "todo",
10411
- // Context notes synonyms
10412
10422
  "context": "context",
10413
10423
  "notes": "context",
10414
10424
  "note": "context",
@@ -10513,6 +10523,8 @@ async function handleMemory(params) {
10513
10523
  return handleFeature(params);
10514
10524
  case "delete_node":
10515
10525
  return handleDeleteNode(params);
10526
+ case "graph_health":
10527
+ return handleGraphHealth(params);
10516
10528
  case "clear": {
10517
10529
  const { clearGraph: clearGraph2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10518
10530
  await clearGraph2();
@@ -10564,14 +10576,26 @@ async function handleResearchSave(params) {
10564
10576
  });
10565
10577
  await saveResearchCache(scope, record, void 0, params.confidence);
10566
10578
  try {
10567
- const { upsertNode: upsertNode2, nodeId: nodeId2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10568
- await upsertNode2({ id: nodeId2("file", scope), type: "file", name: scope });
10569
- await upsertNode2({
10570
- id: `research::${scope}`,
10571
- type: "research",
10572
- name: `research:${scope}`,
10573
- metadata: { confidence: params.confidence || 0.8 }
10574
- });
10579
+ const { getDb: getDb2, flushDb: flushDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
10580
+ const db = await getDb2();
10581
+ const fileId = `file::${scope}`;
10582
+ const researchId = `research::${scope}`;
10583
+ const now = Math.floor(Date.now() / 1e3);
10584
+ db.run(`
10585
+ INSERT INTO nodes (id, type, name, file_path, metadata, updated_at)
10586
+ VALUES (?, 'file', ?, ?, ?, ?)
10587
+ ON CONFLICT(id) DO UPDATE SET
10588
+ metadata = json_patch(COALESCE(nodes.metadata, '{}'), excluded.metadata),
10589
+ updated_at = excluded.updated_at
10590
+ `, [fileId, scope, scope, JSON.stringify({ findings: [params.content || ""], confidence: params.confidence || 0.8 }), now]);
10591
+ db.run(`
10592
+ INSERT INTO nodes (id, type, name, file_path, metadata, updated_at)
10593
+ VALUES (?, 'research', ?, null, ?, ?)
10594
+ ON CONFLICT(id) DO UPDATE SET
10595
+ metadata = json_patch(COALESCE(nodes.metadata, '{}'), excluded.metadata),
10596
+ updated_at = excluded.updated_at
10597
+ `, [researchId, `research:${scope}`, JSON.stringify({ confidence: params.confidence || 0.8, last_findings: params.content || "" }), now]);
10598
+ flushDb2(db);
10575
10599
  } catch {
10576
10600
  }
10577
10601
  try {
@@ -10787,12 +10811,20 @@ async function handleLayerAction(layer, params) {
10787
10811
  \u274C Wrong: hops: file1.js\\nfile2.js
10788
10812
  \u2705 Correct: domain: <name> | hops: file1.js \u2192 file2.js \u2192 file3.js`;
10789
10813
  }
10790
- const hops = hopsStr ? hopsStr.split("\u2192").map((h) => h.trim()).filter(Boolean).map((h, i, arr) => ({
10814
+ const rawHops = hopsStr ? hopsStr.split("\u2192").map((h) => h.trim()).filter(Boolean) : [];
10815
+ const coreHops = rawHops.filter((file) => {
10816
+ if (file.endsWith(".tsx") || file.endsWith(".jsx")) return false;
10817
+ if (file.includes("Controller")) return false;
10818
+ if (file.includes("Schema") || file.includes("RequestSchema")) return false;
10819
+ return true;
10820
+ });
10821
+ const limitedHopsList = coreHops.slice(0, 5);
10822
+ const hops = limitedHopsList.map((h, i, arr) => ({
10791
10823
  from: i === 0 ? domain : arr[i - 1],
10792
10824
  to: h,
10793
10825
  relation: "flows",
10794
10826
  description: h
10795
- })) : [];
10827
+ }));
10796
10828
  if (hops.length === 0) {
10797
10829
  return `\u274C **arch_flow format error:** no hops found after \u2192 separator.
10798
10830
 
@@ -10896,11 +10928,12 @@ async function handleDeleteNode(params) {
10896
10928
  return `\u26A0\uFE0F Unknown scope "${params.scope}". Supported: gotcha, todo, decision, checkpoint`;
10897
10929
  }
10898
10930
  }
10899
- if (params.target) {
10931
+ if (params.target || params.scope) {
10900
10932
  const { flushDb: flushDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
10901
- if (params.target.includes("::") || params.target.includes(":") || params.target.includes("-")) {
10933
+ const targetStr = params.target || "";
10934
+ const targetId = params.scope && !targetStr.includes("::") ? `${targetStr}::${params.scope}` : targetStr;
10935
+ if (targetId && (targetId.includes("::") || targetId.includes(":") || targetId.includes("-") || isNaN(parseInt(targetId, 10)))) {
10902
10936
  try {
10903
- const targetId = params.target;
10904
10937
  const domainName = targetId.startsWith("feature_domain::") ? targetId.replace("feature_domain::", "") : targetId;
10905
10938
  if (targetId.startsWith("feature_domain::")) {
10906
10939
  db.run("DELETE FROM nodes WHERE id LIKE ? OR id LIKE ? OR id LIKE ?", [
@@ -10914,16 +10947,18 @@ async function handleDeleteNode(params) {
10914
10947
  db.run("DELETE FROM nodes WHERE id = ?", [targetId]);
10915
10948
  }
10916
10949
  flushDb2(db);
10917
- return `\u{1F5D1}\uFE0F **Node & relations deleted instantly from RAM and Disk:** ${params.target}`;
10950
+ return `\u{1F5D1}\uFE0F **Node & relations deleted instantly from RAM and Disk:** ${targetId}`;
10918
10951
  } catch (err) {
10919
10952
  return `\u274C Failed to delete node: ${err}`;
10920
10953
  }
10921
10954
  }
10922
- const id = parseInt(params.target, 10);
10923
- if (!isNaN(id)) {
10924
- db.run("DELETE FROM nodes WHERE rowid = ?", [id]);
10925
- flushDb2(db);
10926
- return `\u{1F5D1}\uFE0F **Node #${id} deleted.**`;
10955
+ if (targetId) {
10956
+ const id = parseInt(targetId, 10);
10957
+ if (!isNaN(id)) {
10958
+ db.run("DELETE FROM nodes WHERE rowid = ?", [id]);
10959
+ flushDb2(db);
10960
+ return `\u{1F5D1}\uFE0F **Node #${id} deleted.**`;
10961
+ }
10927
10962
  }
10928
10963
  }
10929
10964
  return "\u26A0\uFE0F Provide `target` (node ID) to delete, or `scope` + `target` (numeric ID) for gotcha/todo/decision/checkpoint.\n\nExamples:\n- `delete_node`, target: 'function::sendMessage'\n- `delete_node`, scope: 'gotcha', target: '42'\n- `delete_node`, scope: 'todo', target: '7'\n- `delete_node`, scope: 'decision', target: '3'\n- `delete_node`, scope: 'checkpoint', target: '1'";
@@ -10984,6 +11019,68 @@ async function handleFeature(params) {
10984
11019
  \u{1F3F7}\uFE0F Tags: ${tags.length}
10985
11020
  \u26A0\uFE0F Risk: ${risk}`;
10986
11021
  }
11022
+ async function handleGraphHealth(_params) {
11023
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
11024
+ const db = await getDb2();
11025
+ const getRows = (sql) => {
11026
+ try {
11027
+ const res = db.exec(sql);
11028
+ if (!res.length) return [];
11029
+ const cols = res[0].columns;
11030
+ return res[0].values.map((val) => {
11031
+ const obj = {};
11032
+ cols.forEach((c, idx) => obj[c] = val[idx]);
11033
+ return obj;
11034
+ });
11035
+ } catch {
11036
+ return [];
11037
+ }
11038
+ };
11039
+ const totalNodesRow = getRows("SELECT COUNT(*) as count FROM nodes")[0];
11040
+ const totalNodes = totalNodesRow ? totalNodesRow.count : 0;
11041
+ const totalEdgesRow = getRows("SELECT COUNT(*) as count FROM edges")[0];
11042
+ const totalEdges = totalEdgesRow ? totalEdgesRow.count : 0;
11043
+ const orphanRows = getRows(`
11044
+ SELECT COUNT(*) as count FROM nodes
11045
+ WHERE id NOT IN (SELECT DISTINCT source_id FROM edges UNION SELECT DISTINCT target_id FROM edges)
11046
+ `);
11047
+ const totalOrphans = orphanRows[0] ? orphanRows[0].count : 0;
11048
+ const duplicateGroups = getRows(`
11049
+ SELECT name, COUNT(*) as count FROM nodes GROUP BY name HAVING count > 1
11050
+ `);
11051
+ const typeStats = getRows(`
11052
+ SELECT type, COUNT(*) as count FROM nodes GROUP BY type ORDER BY count DESC
11053
+ `);
11054
+ const lines = [
11055
+ "\u{1F4CA} **Kuma Knowledge Graph Health Report**",
11056
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
11057
+ `\u2022 **Total Nodes:** ${totalNodes}`,
11058
+ `\u2022 **Total Edges:** ${totalEdges}`,
11059
+ `\u2022 **Orphan Nodes:** ${totalOrphans}`,
11060
+ `\u2022 **Duplicate Groups:** ${duplicateGroups.length}`,
11061
+ "",
11062
+ "**Nodes by Type:**",
11063
+ ...typeStats.map((s) => ` - \`${s.type}\`: ${s.count}`),
11064
+ "",
11065
+ "**Recommendations:**"
11066
+ ];
11067
+ const recs = [];
11068
+ const crossLinks = typeStats.find((s) => s.type === "cross_service_link");
11069
+ if (crossLinks && crossLinks.count > 50) {
11070
+ recs.push("\u26A0\uFE0F Too many `cross_service_link` nodes. Run cleanup or delete obsolete feature domains.");
11071
+ }
11072
+ if (duplicateGroups.length > 0) {
11073
+ recs.push(`\u26A0\uFE0F ${duplicateGroups.length} duplicate node groups detected. Delete duplicates using \`delete_node\`.`);
11074
+ }
11075
+ if (totalOrphans > 10) {
11076
+ recs.push(`\u26A0\uFE0F ${totalOrphans} orphan nodes found (0 edges). Consider running \`heal\` or \`clear\`.`);
11077
+ }
11078
+ if (recs.length === 0) {
11079
+ recs.push("\u2705 Graph health is optimal! Low noise, good signal-to-noise ratio.");
11080
+ }
11081
+ lines.push(...recs.map((r) => ` ${r}`));
11082
+ return lines.join("\n");
11083
+ }
10987
11084
  async function handleLayersSummary(_params) {
10988
11085
  const { getLayersSummary: getLayersSummary2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
10989
11086
  return getLayersSummary2();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.3.24",
3
+ "version": "2.3.25",
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",
@@ -828,15 +828,56 @@ function renderHealth(data){
828
828
  }
829
829
 
830
830
  function setupSearch() {
831
+ var searchTimeout = null;
831
832
  document.getElementById("node-search").oninput = function() {
832
- var query = this.value.toLowerCase().trim();
833
- if (!network || !allNodes) return;
834
- if (!query) { network.body.data.nodes.forEach(function(n){ network.body.data.nodes.update({id:n.id,opacity:1.0}); }); return; }
835
- var matched = new Set();
836
- allNodes.forEach(function(n){
837
- if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) matched.add(n.id);
838
- });
839
- network.body.data.nodes.forEach(function(n){ network.body.data.nodes.update({ id: n.id, opacity: matched.has(n.id) ? 1.0 : 0.15 }); });
833
+ var self = this;
834
+ if (searchTimeout) clearTimeout(searchTimeout);
835
+ searchTimeout = setTimeout(function() {
836
+ var query = self.value.toLowerCase().trim();
837
+ if (!network || !allNodes) return;
838
+
839
+ if (!query) {
840
+ resetHighlight();
841
+ return;
842
+ }
843
+
844
+ var matched = new Set();
845
+ var firstMatchedId = null;
846
+ allNodes.forEach(function(n) {
847
+ if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) {
848
+ matched.add(n.id);
849
+ if (!firstMatchedId) firstMatchedId = n.id;
850
+ }
851
+ });
852
+
853
+ // Update Nodes
854
+ var nodeBatchUpdates = [];
855
+ network.body.data.nodes.forEach(function(n) {
856
+ var hit = matched.has(n.id);
857
+ nodeBatchUpdates.push({ id: n.id, opacity: hit ? 1.0 : 0.08, font: { color: hit ? '#ffffff' : 'rgba(255,255,255,0.04)', bold: hit } });
858
+ });
859
+ network.body.data.nodes.update(nodeBatchUpdates);
860
+
861
+ // Update Edges (Dim edges whose endpoints are both not in search results)
862
+ var edgeBatchUpdates = [];
863
+ network.body.data.edges.forEach(function(e) {
864
+ var hit = matched.has(e.from) || matched.has(e.to);
865
+ var def = edgeDefaults[e.id] || {};
866
+ var strokeColor = hit ? (def.color || '#64748b') : 'rgba(100,116,139,0.05)';
867
+ var fontColor = hit ? '#94a3b8' : 'rgba(148,163,184,0.03)';
868
+ edgeBatchUpdates.push({
869
+ id: e.id,
870
+ color: { color: strokeColor, highlight: '#c084fc', hover: '#38bdf8', opacity: hit ? 0.95 : 0.05 },
871
+ font: { color: fontColor, strokeWidth: 0, background: 'transparent' },
872
+ width: hit ? (def.width || 1.5) : 0.2
873
+ });
874
+ });
875
+ network.body.data.edges.update(edgeBatchUpdates);
876
+
877
+ if (firstMatchedId && matched.size < 10) {
878
+ network.focus(firstMatchedId, { scale: 1.2, animation: { duration: 300 } });
879
+ }
880
+ }, 150);
840
881
  };
841
882
  }
842
883
  </script>