mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(skill): add /understand-explain command for deep-dive file analysis
Adds the explain-builder module that constructs rich context for explaining a specific file or function. Supports file paths and path:function notation, gathers child nodes, connected components, layer membership, and formats a structured prompt for LLM-powered deep-dive explanations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: understand-explain
|
||||
description: Deep-dive explanation of a specific file or function using the knowledge graph
|
||||
arguments: path
|
||||
---
|
||||
|
||||
# /understand-explain
|
||||
|
||||
Provide a thorough, in-depth explanation of a specific code component.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Read the knowledge graph file at `.understand-anything/knowledge-graph.json`
|
||||
2. If it doesn't exist, tell the user to run `/understand` first
|
||||
3. Find the component matching the path: "${ARGUMENTS}"
|
||||
- Supports file paths: `src/auth/login.ts`
|
||||
- Supports function notation: `src/auth/login.ts:verifyToken`
|
||||
4. Analyze the component in context:
|
||||
- Its role in the architecture (which layer, why it exists)
|
||||
- Internal structure (functions, classes it contains)
|
||||
- External connections (what it imports, what calls it, what it depends on)
|
||||
- Data flow (inputs -> processing -> outputs)
|
||||
5. Explain clearly, assuming the reader may not know the programming language
|
||||
6. Highlight any patterns, idioms, or complexity worth understanding
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildExplainContext, formatExplainPrompt } from "../explain-builder.js";
|
||||
import type { KnowledgeGraph } from "@understand-anything/core";
|
||||
|
||||
const sampleGraph: KnowledgeGraph = {
|
||||
version: "1.0.0",
|
||||
project: {
|
||||
name: "test-project",
|
||||
languages: ["typescript"],
|
||||
frameworks: ["express"],
|
||||
description: "A test project",
|
||||
analyzedAt: "2026-03-14T00:00:00Z",
|
||||
gitCommitHash: "abc123",
|
||||
},
|
||||
nodes: [
|
||||
{ id: "file:src/auth.ts", type: "file", name: "auth.ts", filePath: "src/auth.ts", summary: "Auth module", tags: ["auth"], complexity: "complex" },
|
||||
{ id: "func:src/auth.ts:login", type: "function", name: "login", filePath: "src/auth.ts", lineRange: [10, 30], summary: "Login handler", tags: ["auth", "login"], complexity: "moderate" },
|
||||
{ id: "func:src/auth.ts:verify", type: "function", name: "verify", filePath: "src/auth.ts", lineRange: [32, 50], summary: "Token verification", tags: ["auth", "jwt"], complexity: "moderate" },
|
||||
{ id: "file:src/db.ts", type: "file", name: "db.ts", filePath: "src/db.ts", summary: "Database", tags: ["db"], complexity: "simple" },
|
||||
],
|
||||
edges: [
|
||||
{ source: "file:src/auth.ts", target: "func:src/auth.ts:login", type: "contains", direction: "forward", weight: 1.0 },
|
||||
{ source: "file:src/auth.ts", target: "func:src/auth.ts:verify", type: "contains", direction: "forward", weight: 1.0 },
|
||||
{ source: "func:src/auth.ts:login", target: "file:src/db.ts", type: "reads_from", direction: "forward", weight: 0.8 },
|
||||
],
|
||||
layers: [
|
||||
{ id: "layer:auth", name: "Auth Layer", description: "Authentication", nodeIds: ["file:src/auth.ts", "func:src/auth.ts:login", "func:src/auth.ts:verify"] },
|
||||
],
|
||||
tour: [],
|
||||
};
|
||||
|
||||
describe("explain-builder", () => {
|
||||
describe("buildExplainContext", () => {
|
||||
it("finds the file node by path", () => {
|
||||
const ctx = buildExplainContext(sampleGraph, "src/auth.ts");
|
||||
expect(ctx.targetNode?.id).toBe("file:src/auth.ts");
|
||||
});
|
||||
|
||||
it("includes child nodes (functions/classes in the file)", () => {
|
||||
const ctx = buildExplainContext(sampleGraph, "src/auth.ts");
|
||||
expect(ctx.childNodes.map((n) => n.name)).toContain("login");
|
||||
expect(ctx.childNodes.map((n) => n.name)).toContain("verify");
|
||||
});
|
||||
|
||||
it("includes connected nodes", () => {
|
||||
const ctx = buildExplainContext(sampleGraph, "src/auth.ts");
|
||||
const allIds = ctx.connectedNodes.map((n) => n.id);
|
||||
expect(allIds).toContain("file:src/db.ts");
|
||||
});
|
||||
|
||||
it("includes the layer", () => {
|
||||
const ctx = buildExplainContext(sampleGraph, "src/auth.ts");
|
||||
expect(ctx.layer?.name).toBe("Auth Layer");
|
||||
});
|
||||
|
||||
it("returns null targetNode for unknown paths", () => {
|
||||
const ctx = buildExplainContext(sampleGraph, "src/unknown.ts");
|
||||
expect(ctx.targetNode).toBeNull();
|
||||
});
|
||||
|
||||
it("finds function nodes by partial path match", () => {
|
||||
const ctx = buildExplainContext(sampleGraph, "src/auth.ts:login");
|
||||
expect(ctx.targetNode?.name).toBe("login");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatExplainPrompt", () => {
|
||||
it("produces structured markdown for valid context", () => {
|
||||
const ctx = buildExplainContext(sampleGraph, "src/auth.ts");
|
||||
const prompt = formatExplainPrompt(ctx);
|
||||
expect(prompt).toContain("auth.ts");
|
||||
expect(prompt).toContain("login");
|
||||
expect(prompt).toContain("Auth Layer");
|
||||
});
|
||||
|
||||
it("produces helpful message for unknown path", () => {
|
||||
const ctx = buildExplainContext(sampleGraph, "src/unknown.ts");
|
||||
const prompt = formatExplainPrompt(ctx);
|
||||
expect(prompt).toContain("not found");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
import type {
|
||||
KnowledgeGraph,
|
||||
GraphNode,
|
||||
GraphEdge,
|
||||
Layer,
|
||||
} from "@understand-anything/core";
|
||||
|
||||
export interface ExplainContext {
|
||||
projectName: string;
|
||||
path: string;
|
||||
targetNode: GraphNode | null;
|
||||
childNodes: GraphNode[];
|
||||
connectedNodes: GraphNode[];
|
||||
relevantEdges: GraphEdge[];
|
||||
layer: Layer | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a context for explaining a specific file or function.
|
||||
* Supports file paths ("src/auth.ts") and path:function ("src/auth.ts:login").
|
||||
*/
|
||||
export function buildExplainContext(
|
||||
graph: KnowledgeGraph,
|
||||
path: string,
|
||||
): ExplainContext {
|
||||
const { nodes, edges, layers } = graph;
|
||||
|
||||
let targetNode: GraphNode | null = null;
|
||||
|
||||
// Check for path:function format (e.g. "src/auth.ts:login")
|
||||
const colonIdx = path.lastIndexOf(":");
|
||||
if (colonIdx > 0 && !path.includes("://")) {
|
||||
const filePath = path.slice(0, colonIdx);
|
||||
const funcName = path.slice(colonIdx + 1);
|
||||
targetNode =
|
||||
nodes.find(
|
||||
(n) => n.filePath === filePath && n.name === funcName,
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
// Fall back to file path match
|
||||
if (!targetNode) {
|
||||
targetNode = nodes.find((n) => n.filePath === path) ?? null;
|
||||
}
|
||||
|
||||
if (!targetNode) {
|
||||
return {
|
||||
projectName: graph.project.name,
|
||||
path,
|
||||
targetNode: null,
|
||||
childNodes: [],
|
||||
connectedNodes: [],
|
||||
relevantEdges: [],
|
||||
layer: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Find child nodes (contained by this node via "contains" edges)
|
||||
const childNodes = nodes.filter((n) =>
|
||||
edges.some(
|
||||
(e) =>
|
||||
e.source === targetNode!.id &&
|
||||
e.target === n.id &&
|
||||
e.type === "contains",
|
||||
),
|
||||
);
|
||||
|
||||
const allRelatedIds = new Set([
|
||||
targetNode.id,
|
||||
...childNodes.map((n) => n.id),
|
||||
]);
|
||||
|
||||
// Find connected nodes (1-hop neighbors, excluding children and self)
|
||||
const connectedIds = new Set<string>();
|
||||
const relevantEdges: GraphEdge[] = [];
|
||||
|
||||
for (const edge of edges) {
|
||||
if (allRelatedIds.has(edge.source) || allRelatedIds.has(edge.target)) {
|
||||
relevantEdges.push(edge);
|
||||
if (allRelatedIds.has(edge.source) && !allRelatedIds.has(edge.target)) {
|
||||
connectedIds.add(edge.target);
|
||||
}
|
||||
if (allRelatedIds.has(edge.target) && !allRelatedIds.has(edge.source)) {
|
||||
connectedIds.add(edge.source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const connectedNodes = nodes.filter((n) => connectedIds.has(n.id));
|
||||
|
||||
const layer =
|
||||
layers.find((l) => l.nodeIds.includes(targetNode!.id)) ?? null;
|
||||
|
||||
return {
|
||||
projectName: graph.project.name,
|
||||
path,
|
||||
targetNode,
|
||||
childNodes,
|
||||
connectedNodes,
|
||||
relevantEdges,
|
||||
layer,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the explain context as a structured prompt for LLM consumption.
|
||||
*/
|
||||
export function formatExplainPrompt(ctx: ExplainContext): string {
|
||||
if (!ctx.targetNode) {
|
||||
return [
|
||||
`# Component Not Found`,
|
||||
``,
|
||||
`The path "${ctx.path}" was not found in the knowledge graph for ${ctx.projectName}.`,
|
||||
``,
|
||||
`Possible reasons:`,
|
||||
`- The file hasn't been analyzed yet — try running /understand first`,
|
||||
`- The path may be different in the graph — check the exact file path`,
|
||||
`- The file may have been deleted or renamed since the last analysis`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
const { targetNode, childNodes, connectedNodes, relevantEdges, layer } = ctx;
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`# Deep Dive: ${targetNode.name}`);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`**Type:** ${targetNode.type} | **Complexity:** ${targetNode.complexity}`,
|
||||
);
|
||||
if (targetNode.filePath)
|
||||
lines.push(`**File:** \`${targetNode.filePath}\``);
|
||||
if (targetNode.lineRange)
|
||||
lines.push(
|
||||
`**Lines:** ${targetNode.lineRange[0]}-${targetNode.lineRange[1]}`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push(`**Summary:** ${targetNode.summary}`);
|
||||
lines.push("");
|
||||
|
||||
if (layer) {
|
||||
lines.push(`## Architectural Layer: ${layer.name}`);
|
||||
lines.push(layer.description);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (childNodes.length > 0) {
|
||||
lines.push("## Internal Components");
|
||||
for (const child of childNodes) {
|
||||
lines.push(`- **${child.name}** (${child.type}): ${child.summary}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (connectedNodes.length > 0) {
|
||||
lines.push("## Connected Components");
|
||||
for (const node of connectedNodes) {
|
||||
lines.push(`- **${node.name}** (${node.type}): ${node.summary}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (relevantEdges.length > 0) {
|
||||
const nodeMap = new Map(
|
||||
[...[targetNode], ...childNodes, ...connectedNodes].map((n) => [
|
||||
n.id,
|
||||
n,
|
||||
]),
|
||||
);
|
||||
lines.push("## Relationships");
|
||||
for (const edge of relevantEdges) {
|
||||
if (edge.type === "contains") continue;
|
||||
const src = nodeMap.get(edge.source)?.name ?? edge.source;
|
||||
const tgt = nodeMap.get(edge.target)?.name ?? edge.target;
|
||||
const desc = edge.description ? ` — ${edge.description}` : "";
|
||||
lines.push(`- ${src} --[${edge.type}]--> ${tgt}${desc}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (targetNode.languageNotes) {
|
||||
lines.push("## Language Notes");
|
||||
lines.push(targetNode.languageNotes);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("## Instructions");
|
||||
lines.push("Provide a thorough explanation of this component:");
|
||||
lines.push("1. What it does and why it exists in the project");
|
||||
lines.push("2. How data flows through it (inputs, processing, outputs)");
|
||||
lines.push("3. How it interacts with connected components");
|
||||
lines.push("4. Any patterns, idioms, or design decisions worth noting");
|
||||
lines.push("5. Potential gotchas or areas of complexity");
|
||||
lines.push("");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -9,3 +9,8 @@ export {
|
||||
formatDiffAnalysis,
|
||||
type DiffContext,
|
||||
} from "./diff-analyzer.js";
|
||||
export {
|
||||
buildExplainContext,
|
||||
formatExplainPrompt,
|
||||
type ExplainContext,
|
||||
} from "./explain-builder.js";
|
||||
|
||||
Reference in New Issue
Block a user