mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(dashboard): surface graph validation issues and add messy graph fixtures
Add a --messy mode to the large graph generator so robustness cases can be tested with null fields, aliases, dangling refs, and other malformed data. Update the dashboard to preserve non-fatal validation issues, log auto-corrected and dropped items, and show a warning banner with expandable details plus copy-to-clipboard support.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [graphIssues, setGraphIssues] = useState<GraphIssue[]>([]);
|
||||
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 */}
|
||||
<SearchBar />
|
||||
|
||||
{/* Validation warning banner */}
|
||||
{graphIssues.length > 0 && !loadError && (
|
||||
<WarningBanner issues={graphIssues} />
|
||||
)}
|
||||
|
||||
{/* Error banner */}
|
||||
{loadError && (
|
||||
<div className="px-5 py-3 bg-red-900/30 border-b border-red-700 text-red-200 text-sm">
|
||||
|
||||
@@ -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<string, number> = { "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 (
|
||||
<div className="bg-amber-900/20 border-b border-amber-700 text-amber-200 text-sm">
|
||||
{/* Collapsed summary row */}
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
className="w-full flex items-center gap-2 px-5 py-3 text-left hover:bg-amber-900/10 transition-colors"
|
||||
>
|
||||
{/* Chevron icon */}
|
||||
<svg
|
||||
className={`w-4 h-4 shrink-0 text-amber-400 transition-transform duration-200 ${
|
||||
expanded ? "rotate-90" : ""
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Warning icon */}
|
||||
<svg
|
||||
className="w-4 h-4 shrink-0 text-amber-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="flex-1">{summary}</span>
|
||||
|
||||
<span className="text-amber-400/60 text-xs shrink-0">
|
||||
{expanded ? "click to collapse" : "click to expand"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Expanded detail panel */}
|
||||
{expanded && (
|
||||
<div className="px-5 pb-4">
|
||||
{/* Issue list */}
|
||||
<div className="space-y-1 mb-3">
|
||||
{/* Auto-corrected issues */}
|
||||
{autoCorrected.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-amber-400 mb-1">
|
||||
Auto-corrected ({autoCorrected.length})
|
||||
</h4>
|
||||
{autoCorrected.map((issue, i) => (
|
||||
<div key={`ac-${i}`} className="flex items-start gap-2 py-0.5 pl-2 text-amber-200/80">
|
||||
<span className="text-amber-400 shrink-0 mt-0.5">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className="text-xs">{issue.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dropped issues */}
|
||||
{dropped.length > 0 && (
|
||||
<div className={autoCorrected.length > 0 ? "mt-2" : ""}>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-orange-400 mb-1">
|
||||
Dropped ({dropped.length})
|
||||
</h4>
|
||||
{dropped.map((issue, i) => (
|
||||
<div key={`dr-${i}`} className="flex items-start gap-2 py-0.5 pl-2 text-orange-300/80">
|
||||
<span className="text-orange-400 shrink-0 mt-0.5">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className="text-xs">{issue.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer with copy button and actionable message */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-amber-700/50">
|
||||
<p className="text-xs text-amber-200/60">
|
||||
Copy these issues and ask your agent to fix them in knowledge-graph.json
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1.5 px-3 py-1 rounded text-xs font-medium bg-amber-800/40 text-amber-200 hover:bg-amber-800/60 transition-colors shrink-0 ml-4"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Copy Issues
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user