diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6372323..140122a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,8 +9,8 @@ { "name": "understand-anything", "description": "Multi-agent codebase analysis with interactive dashboard, guided tours, and skill commands", - "version": "1.2.0", + "version": "1.2.1", "source": "./understand-anything-plugin" } ] -} \ No newline at end of file +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b496896..5cabce8 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "understand-anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "1.2.0", + "version": "1.2.1", "author": { "name": "Lum1104" }, @@ -15,4 +15,4 @@ "onboarding", "dashboard" ] -} \ No newline at end of file +} diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 006ddb2..41f5046 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "understand-anything", "displayName": "Understand Anything", "description": "AI-powered codebase understanding — analyze, visualize, and explain any project", - "version": "1.2.0", + "version": "1.2.1", "author": { "name": "Lum1104" }, diff --git a/scripts/generate-large-graph.mjs b/scripts/generate-large-graph.mjs index 8a18360..436063f 100644 --- a/scripts/generate-large-graph.mjs +++ b/scripts/generate-large-graph.mjs @@ -1,10 +1,15 @@ #!/usr/bin/env node /** - * Generate a large fake knowledge graph for testing PR #18 - * (Web Worker layout for large graphs). + * Generate a large fake knowledge graph for testing. * * Usage: * node scripts/generate-large-graph.mjs [nodeCount] + * node scripts/generate-large-graph.mjs [nodeCount] --messy + * + * Flags: + * --messy Inject LLM-style issues into ~20% of nodes/edges to test the + * dashboard robustness pipeline (Tier 1-3: null fields, wrong cases, + * missing fields, aliases, dangling refs, unrecognizable types). * * Default: 3000 nodes. Writes to .understand-anything/knowledge-graph.json */ @@ -12,7 +17,10 @@ import { writeFileSync, mkdirSync } from "node:fs"; import { resolve } from "node:path"; -const NODE_COUNT = parseInt(process.argv[2] || "3000", 10); +const args = process.argv.slice(2); +const MESSY = args.includes("--messy"); +const numArg = args.find((a) => !a.startsWith("--")); +const NODE_COUNT = parseInt(numArg || "3000", 10); const EDGE_RATIO = 1.7; // edges per node (realistic for codebases) const nodeTypes = ["file", "function", "class", "module", "concept"]; @@ -110,6 +118,137 @@ function generateTour(nodes) { return steps; } +// ── Messy injection (--messy flag) ── + +// Tier 1: silent fixes — null optional fields, mixed-case enums +function injectTier1(node) { + const issues = []; + if (Math.random() < 0.5 && node.filePath !== undefined) { + node.filePath = null; // null on optional field + issues.push("null filePath"); + } + if (Math.random() < 0.5) { + node.type = node.type.toUpperCase(); // "FILE", "FUNCTION" + issues.push(`uppercase type "${node.type}"`); + } + if (Math.random() < 0.5) { + node.complexity = node.complexity[0].toUpperCase() + node.complexity.slice(1); // "Simple" + issues.push(`mixed-case complexity "${node.complexity}"`); + } + return issues; +} + +// Tier 2: auto-fixable — missing fields, aliases, string weights +function injectTier2Node(node) { + const issues = []; + const r = Math.random(); + if (r < 0.2) { + delete node.complexity; + issues.push("missing complexity"); + } else if (r < 0.4) { + node.complexity = pick(["low", "easy", "medium", "intermediate", "high", "hard"]); + issues.push(`complexity alias "${node.complexity}"`); + } + if (Math.random() < 0.3) { + delete node.tags; + issues.push("missing tags"); + } + if (Math.random() < 0.2) { + delete node.summary; + issues.push("missing summary"); + } + if (Math.random() < 0.15) { + node.type = pick(["func", "fn", "method", "interface", "struct", "mod", "pkg"]); + issues.push(`type alias "${node.type}"`); + } + return issues; +} + +function injectTier2Edge(edge) { + const issues = []; + if (Math.random() < 0.3) { + edge.weight = String(edge.weight); // string weight + issues.push(`string weight "${edge.weight}"`); + } + if (Math.random() < 0.2) { + delete edge.direction; + issues.push("missing direction"); + } else if (Math.random() < 0.3) { + edge.direction = pick(["to", "outbound", "from", "inbound", "both"]); + issues.push(`direction alias "${edge.direction}"`); + } + if (Math.random() < 0.15) { + edge.type = pick(["extends", "invokes", "uses", "requires", "relates_to"]); + issues.push(`edge type alias "${edge.type}"`); + } + return issues; +} + +// Tier 3: unrecoverable — missing id/name, dangling refs, bad types +function injectTier3Node(node) { + const r = Math.random(); + if (r < 0.4) { + delete node.id; + return "missing id"; + } else if (r < 0.7) { + delete node.name; + return "missing name"; + } else { + node.type = "totally_bogus_type"; + return `unrecognizable type "${node.type}"`; + } +} + +function injectTier3Edge(edge, validNodeIds) { + const r = Math.random(); + if (r < 0.4) { + edge.target = "nonexistent-node-999999"; + return "dangling target ref"; + } else if (r < 0.7) { + edge.source = "nonexistent-node-888888"; + return "dangling source ref"; + } else { + edge.weight = "not_a_number"; + return "non-coercible weight"; + } +} + +function applyMessy(nodes, edges) { + const stats = { tier1: 0, tier2: 0, tier3: 0 }; + + for (const node of nodes) { + const r = Math.random(); + if (r < 0.10) { + // ~10% get Tier 3 issues (will be dropped) + injectTier3Node(node); + stats.tier3++; + } else if (r < 0.30) { + // ~20% get Tier 2 issues (will be auto-corrected) + injectTier2Node(node); + stats.tier2++; + } else if (r < 0.40) { + // ~10% get Tier 1 issues (silently fixed) + injectTier1(node); + stats.tier1++; + } + } + + const validIds = new Set(nodes.filter((n) => n.id).map((n) => n.id)); + for (const edge of edges) { + const r = Math.random(); + if (r < 0.05) { + injectTier3Edge(edge, validIds); + stats.tier3++; + } else if (r < 0.20) { + injectTier2Edge(edge); + stats.tier2++; + } + } + + // Also set tour/layers to null (Tier 1 null-vs-empty) + return stats; +} + // ── Generate ── const nodes = generateNodes(NODE_COUNT); @@ -118,20 +257,25 @@ const edges = generateEdges(nodes, edgeCount); const layers = generateLayers(nodes); const tour = generateTour(nodes); +let messyStats = null; +if (MESSY) { + messyStats = applyMessy(nodes, edges); +} + const graph = { version: "1.0", project: { name: "large-test-project", languages: languages.slice(0, 3), frameworks: frameworks.slice(0, 2), - description: `Auto-generated project with ${NODE_COUNT} nodes for performance testing.`, + description: `Auto-generated project with ${NODE_COUNT} nodes for ${MESSY ? "robustness" : "performance"} testing.`, analyzedAt: new Date().toISOString(), gitCommitHash: "0000000000000000000000000000000000000000", }, nodes, edges, - layers, - tour, + layers: MESSY && Math.random() < 0.5 ? null : layers, + tour: MESSY && Math.random() < 0.5 ? null : tour, }; const outDir = resolve(process.cwd(), ".understand-anything"); @@ -139,9 +283,15 @@ mkdirSync(outDir, { recursive: true }); const outPath = resolve(outDir, "knowledge-graph.json"); writeFileSync(outPath, JSON.stringify(graph, null, 2)); -console.log(`Generated knowledge graph:`); +console.log(`Generated knowledge graph${MESSY ? " (messy mode)" : ""}:`); console.log(` Nodes: ${nodes.length}`); console.log(` Edges: ${edges.length}`); -console.log(` Layers: ${layers.length}`); -console.log(` Tour steps: ${tour.length}`); +console.log(` Layers: ${graph.layers === null ? "null (Tier 1 test)" : layers.length}`); +console.log(` Tour steps: ${graph.tour === null ? "null (Tier 1 test)" : tour.length}`); +if (messyStats) { + console.log(` Injected issues:`); + console.log(` Tier 1 (silent fix): ~${messyStats.tier1} items`); + console.log(` Tier 2 (auto-correct): ~${messyStats.tier2} items`); + console.log(` Tier 3 (will be dropped): ~${messyStats.tier3} items`); +} console.log(` Written to: ${outPath}`); diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json index 7245516..56906b1 100644 --- a/understand-anything-plugin/package.json +++ b/understand-anything-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@understand-anything/skill", - "version": "1.2.0", + "version": "1.2.1", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -16,4 +16,4 @@ "typescript": "^5.7.0", "vitest": "^3.1.0" } -} \ No newline at end of file +} diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 3f1f265..58fef79 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, useMemo } from "react"; import { validateGraph } from "@understand-anything/core/schema"; +import type { GraphIssue } from "@understand-anything/core/schema"; import { useDashboardStore } from "./store"; import GraphView from "./components/GraphView"; import CodeViewer from "./components/CodeViewer"; @@ -11,6 +12,7 @@ import LearnPanel from "./components/LearnPanel"; import PersonaSelector from "./components/PersonaSelector"; import ProjectOverview from "./components/ProjectOverview"; import KeyboardShortcutsHelp from "./components/KeyboardShortcutsHelp"; +import WarningBanner from "./components/WarningBanner"; import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts"; @@ -24,6 +26,7 @@ function App() { const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer); const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay); const [loadError, setLoadError] = useState(null); + const [graphIssues, setGraphIssues] = useState([]); const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); // Define keyboard shortcuts @@ -123,10 +126,20 @@ function App() { const result = validateGraph(data); if (result.success && result.data) { setGraph(result.data); + setGraphIssues(result.issues); + for (const issue of result.issues) { + if (issue.level === "auto-corrected") { + console.warn(`[graph] auto-corrected: ${issue.message}`); + } else if (issue.level === "dropped") { + console.error(`[graph] dropped: ${issue.message}`); + } + } + } else if (result.fatal) { + console.error("Knowledge graph validation failed:", result.fatal); + setLoadError(`Invalid knowledge graph: ${result.fatal}`); } else { - const errorMsg = result.errors?.join("; ") ?? "Unknown validation error"; - console.error("Knowledge graph validation failed:", errorMsg); - setLoadError(`Invalid knowledge graph: ${errorMsg}`); + console.error("Knowledge graph validation failed: unknown error"); + setLoadError("Invalid knowledge graph: unknown validation error"); } }) .catch((err) => { @@ -210,6 +223,11 @@ function App() { {/* Search */} + {/* Validation warning banner */} + {graphIssues.length > 0 && !loadError && ( + + )} + {/* Error banner */} {loadError && (
diff --git a/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx b/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx new file mode 100644 index 0000000..753a2c5 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx @@ -0,0 +1,193 @@ +import { useState, useCallback } from "react"; +import type { GraphIssue } from "@understand-anything/core/schema"; + +interface WarningBannerProps { + issues: GraphIssue[]; +} + +function buildCopyText(issues: GraphIssue[]): string { + const lines = [ + "The following issues were found in your knowledge-graph.json.", + "These are LLM generation errors — not a system bug.", + "You can ask your agent to fix these specific issues in the knowledge-graph.json file:", + "", + ]; + + // Auto-corrected first, then dropped + const sorted = [...issues].sort((a, b) => { + const order: Record = { "auto-corrected": 0, dropped: 1, fatal: 2 }; + return (order[a.level] ?? 2) - (order[b.level] ?? 2); + }); + + for (const issue of sorted) { + const label = + issue.level === "auto-corrected" + ? "Auto-corrected" + : issue.level === "dropped" + ? "Dropped" + : "Fatal"; + lines.push(`[${label}] ${issue.message}`); + } + + return lines.join("\n"); +} + +export default function WarningBanner({ issues }: WarningBannerProps) { + const [expanded, setExpanded] = useState(false); + const [copied, setCopied] = useState(false); + + const autoCorrected = issues.filter((i) => i.level === "auto-corrected"); + const dropped = issues.filter((i) => i.level === "dropped"); + + // Build summary text — only mention counts > 0 + const parts: string[] = []; + if (autoCorrected.length > 0) { + parts.push(`${autoCorrected.length} auto-correction${autoCorrected.length !== 1 ? "s" : ""}`); + } + if (dropped.length > 0) { + parts.push(`${dropped.length} dropped item${dropped.length !== 1 ? "s" : ""}`); + } + const summary = `Knowledge graph loaded with ${parts.join(" and ")}`; + + const handleCopy = useCallback(async () => { + const text = buildCopyText(issues); + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + console.warn("Clipboard write failed — copy text manually from the expanded issue list"); + } + }, [issues]); + + if (issues.length === 0) return null; + + return ( +
+ {/* Collapsed summary row */} + + + {/* Expanded detail panel */} + {expanded && ( +
+ {/* Issue list */} +
+ {/* Auto-corrected issues */} + {autoCorrected.length > 0 && ( +
+

+ Auto-corrected ({autoCorrected.length}) +

+ {autoCorrected.map((issue, i) => ( +
+ + + + + + {issue.message} +
+ ))} +
+ )} + + {/* Dropped issues */} + {dropped.length > 0 && ( +
0 ? "mt-2" : ""}> +

+ Dropped ({dropped.length}) +

+ {dropped.map((issue, i) => ( +
+ + + + + + {issue.message} +
+ ))} +
+ )} +
+ + {/* Footer with copy button and actionable message */} +
+

+ Copy these issues and ask your agent to fix them in knowledge-graph.json +

+ +
+
+ )} +
+ ); +}