From eb0d2eaac0b75c58d5a0c62e264f9030bbbd3b8f Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 14 Mar 2026 22:31:09 +0800 Subject: [PATCH] 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 --- .../.claude/skills/understand-explain.md | 24 +++ .../src/__tests__/explain-builder.test.ts | 82 ++++++++ packages/skill/src/explain-builder.ts | 196 ++++++++++++++++++ packages/skill/src/index.ts | 5 + 4 files changed, 307 insertions(+) create mode 100644 packages/skill/.claude/skills/understand-explain.md create mode 100644 packages/skill/src/__tests__/explain-builder.test.ts create mode 100644 packages/skill/src/explain-builder.ts diff --git a/packages/skill/.claude/skills/understand-explain.md b/packages/skill/.claude/skills/understand-explain.md new file mode 100644 index 0000000..f446a2f --- /dev/null +++ b/packages/skill/.claude/skills/understand-explain.md @@ -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 diff --git a/packages/skill/src/__tests__/explain-builder.test.ts b/packages/skill/src/__tests__/explain-builder.test.ts new file mode 100644 index 0000000..d95474a --- /dev/null +++ b/packages/skill/src/__tests__/explain-builder.test.ts @@ -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"); + }); + }); +}); diff --git a/packages/skill/src/explain-builder.ts b/packages/skill/src/explain-builder.ts new file mode 100644 index 0000000..7360963 --- /dev/null +++ b/packages/skill/src/explain-builder.ts @@ -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(); + 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"); +} diff --git a/packages/skill/src/index.ts b/packages/skill/src/index.ts index e953523..d9bbcac 100644 --- a/packages/skill/src/index.ts +++ b/packages/skill/src/index.ts @@ -9,3 +9,8 @@ export { formatDiffAnalysis, type DiffContext, } from "./diff-analyzer.js"; +export { + buildExplainContext, + formatExplainPrompt, + type ExplainContext, +} from "./explain-builder.js";