commit 5d20a57971bdd8f869803e6506e055998bcb0f97 Author: Lum1104 Date: Sat Mar 14 17:15:51 2026 +0800 docs: add design doc and Phase 1 implementation plan Co-Authored-By: Claude Opus 4.6 diff --git a/docs/plans/2026-03-14-phase1-implementation.md b/docs/plans/2026-03-14-phase1-implementation.md new file mode 100644 index 0000000..c85b24a --- /dev/null +++ b/docs/plans/2026-03-14-phase1-implementation.md @@ -0,0 +1,2469 @@ +# Understand Anything — Phase 1 Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Build the foundational MVP — a pnpm monorepo with a core analysis engine (LLM + tree-sitter), a `/understand` skill command, and a basic React dashboard with graph view and code viewer. + +**Architecture:** Monorepo with 3 packages (core, skill, dashboard) sharing a knowledge graph JSON schema. The core package handles analysis and persistence. The skill invokes core and launches the dashboard. The dashboard reads the JSON and renders a multi-panel workspace. + +**Tech Stack:** TypeScript, pnpm workspaces, Vitest, React 18, Vite, @xyflow/react (React Flow v12), @monaco-editor/react, Zustand, TailwindCSS, tree-sitter + +--- + +## Task 1: Project Scaffolding — Monorepo Root + +**Files:** +- Create: `package.json` +- Create: `pnpm-workspace.yaml` +- Create: `tsconfig.json` +- Create: `.gitignore` +- Create: `.npmrc` + +**Step 1: Create root package.json** + +```json +{ + "name": "understand-anything", + "private": true, + "type": "module", + "packageManager": "pnpm@10.6.2", + "scripts": { + "build": "pnpm -r build", + "test": "vitest", + "dev:dashboard": "pnpm --filter @understand-anything/dashboard dev", + "lint": "eslint ." + }, + "devDependencies": { + "typescript": "^5.7.0", + "vitest": "^3.1.0" + } +} +``` + +**Step 2: Create pnpm-workspace.yaml** + +```yaml +packages: + - 'packages/*' +``` + +**Step 3: Create root tsconfig.json** + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src" + } +} +``` + +**Step 4: Create .gitignore** + +``` +node_modules/ +dist/ +.understand-anything/ +*.tsbuildinfo +.DS_Store +``` + +**Step 5: Create .npmrc** + +``` +shamefully-hoist=false +strict-peer-dependencies=false +``` + +**Step 6: Run pnpm install** + +Run: `pnpm install` +Expected: lockfile created, no errors + +**Step 7: Commit** + +```bash +git add package.json pnpm-workspace.yaml tsconfig.json .gitignore .npmrc pnpm-lock.yaml +git commit -m "chore: scaffold monorepo root with pnpm workspaces" +``` + +--- + +## Task 2: Core Package — Scaffolding & Knowledge Graph Types + +**Files:** +- Create: `packages/core/package.json` +- Create: `packages/core/tsconfig.json` +- Create: `packages/core/src/index.ts` +- Create: `packages/core/src/types.ts` + +**Step 1: Create packages/core/package.json** + +```json +{ + "name": "@understand-anything/core", + "version": "0.1.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "vitest run" + }, + "devDependencies": { + "typescript": "^5.7.0", + "vitest": "^3.1.0" + } +} +``` + +**Step 2: Create packages/core/tsconfig.json** + +```json +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} +``` + +**Step 3: Create packages/core/src/types.ts** + +This is the full Knowledge Graph type system from the design doc: + +```typescript +// === Edge Types === + +export type EdgeType = + // Structural + | "imports" + | "exports" + | "contains" + | "inherits" + | "implements" + // Behavioral + | "calls" + | "subscribes" + | "publishes" + | "middleware" + // Data flow + | "reads_from" + | "writes_to" + | "transforms" + | "validates" + // Dependencies + | "depends_on" + | "tested_by" + | "configures" + // Semantic + | "related" + | "similar_to"; + +// === Graph Node === + +export interface GraphNode { + id: string; + type: "file" | "function" | "class" | "module" | "concept"; + name: string; + filePath?: string; + lineRange?: [number, number]; + summary: string; + tags: string[]; + complexity: "simple" | "moderate" | "complex"; + languageNotes?: string; +} + +// === Graph Edge === + +export interface GraphEdge { + source: string; + target: string; + type: EdgeType; + direction: "forward" | "backward" | "bidirectional"; + description?: string; + weight: number; +} + +// === Layer === + +export interface Layer { + id: string; + name: string; + description: string; + nodeIds: string[]; +} + +// === Tour Step === + +export interface TourStep { + order: number; + title: string; + description: string; + nodeIds: string[]; + languageLesson?: string; +} + +// === Project Metadata === + +export interface ProjectMeta { + name: string; + languages: string[]; + frameworks: string[]; + description: string; + analyzedAt: string; + gitCommitHash: string; +} + +// === Knowledge Graph (root) === + +export interface KnowledgeGraph { + version: string; + project: ProjectMeta; + nodes: GraphNode[]; + edges: GraphEdge[]; + layers: Layer[]; + tour: TourStep[]; +} + +// === Analysis Metadata === + +export interface AnalysisMeta { + lastAnalyzedAt: string; + gitCommitHash: string; + version: string; + analyzedFiles: number; +} + +// === Plugin Interface === + +export interface StructuralAnalysis { + functions: Array<{ + name: string; + lineRange: [number, number]; + params: string[]; + returnType?: string; + }>; + classes: Array<{ + name: string; + lineRange: [number, number]; + methods: string[]; + properties: string[]; + }>; + imports: Array<{ + source: string; + specifiers: string[]; + lineNumber: number; + }>; + exports: Array<{ + name: string; + lineNumber: number; + }>; +} + +export interface ImportResolution { + source: string; + resolvedPath: string; + specifiers: string[]; +} + +export interface CallGraphEntry { + caller: string; + callee: string; + lineNumber: number; +} + +export interface AnalyzerPlugin { + name: string; + languages: string[]; + analyzeFile(filePath: string, content: string): StructuralAnalysis; + resolveImports(filePath: string, content: string): ImportResolution[]; + extractCallGraph?(filePath: string, content: string): CallGraphEntry[]; +} +``` + +**Step 4: Create packages/core/src/index.ts** + +```typescript +export * from "./types.js"; +``` + +**Step 5: Run pnpm install and build** + +Run: `cd /path/to/project && pnpm install && pnpm --filter @understand-anything/core build` +Expected: Compiles with no errors, `packages/core/dist/` created + +**Step 6: Write a type validation test** + +Create: `packages/core/src/types.test.ts` + +```typescript +import { describe, it, expect } from "vitest"; +import type { + KnowledgeGraph, + GraphNode, + GraphEdge, + ProjectMeta, +} from "./types.js"; + +describe("KnowledgeGraph types", () => { + it("should create a valid empty knowledge graph", () => { + const graph: KnowledgeGraph = { + version: "1.0.0", + project: { + name: "test-project", + languages: ["typescript"], + frameworks: [], + description: "A test project", + analyzedAt: new Date().toISOString(), + gitCommitHash: "abc123", + }, + nodes: [], + edges: [], + layers: [], + tour: [], + }; + + expect(graph.version).toBe("1.0.0"); + expect(graph.nodes).toHaveLength(0); + }); + + it("should create valid graph nodes", () => { + const node: GraphNode = { + id: "node-1", + type: "function", + name: "handleLogin", + filePath: "src/auth/login.ts", + lineRange: [10, 25], + summary: "Handles user login with email and password", + tags: ["auth", "login", "api"], + complexity: "moderate", + languageNotes: "Uses async/await for API calls", + }; + + expect(node.type).toBe("function"); + expect(node.tags).toContain("auth"); + }); + + it("should create valid graph edges", () => { + const edge: GraphEdge = { + source: "node-1", + target: "node-2", + type: "calls", + direction: "forward", + description: "handleLogin calls validateCredentials", + weight: 0.8, + }; + + expect(edge.type).toBe("calls"); + expect(edge.weight).toBeGreaterThan(0); + expect(edge.weight).toBeLessThanOrEqual(1); + }); +}); +``` + +**Step 7: Run tests** + +Run: `pnpm --filter @understand-anything/core test` +Expected: All 3 tests PASS + +**Step 8: Commit** + +```bash +git add packages/core/ +git commit -m "feat(core): add knowledge graph type system and validation tests" +``` + +--- + +## Task 3: Core Package — JSON Persistence + +**Files:** +- Create: `packages/core/src/persistence/index.ts` +- Create: `packages/core/src/persistence/persistence.test.ts` + +**Step 1: Write the failing test** + +Create: `packages/core/src/persistence/persistence.test.ts` + +```typescript +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { loadGraph, saveGraph, loadMeta, saveMeta } from "./index.js"; +import type { KnowledgeGraph, AnalysisMeta } from "../types.js"; + +describe("Persistence", () => { + let testDir: string; + + beforeEach(() => { + testDir = join(tmpdir(), `ua-test-${Date.now()}`); + mkdirSync(testDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + const makeGraph = (): KnowledgeGraph => ({ + version: "1.0.0", + project: { + name: "test", + languages: ["typescript"], + frameworks: [], + description: "test project", + analyzedAt: new Date().toISOString(), + gitCommitHash: "abc123", + }, + nodes: [ + { + id: "n1", + type: "file", + name: "index.ts", + summary: "Entry point", + tags: ["entry"], + complexity: "simple", + }, + ], + edges: [], + layers: [], + tour: [], + }); + + it("saveGraph writes knowledge-graph.json", () => { + const graph = makeGraph(); + saveGraph(testDir, graph); + + const filePath = join(testDir, ".understand-anything", "knowledge-graph.json"); + expect(existsSync(filePath)).toBe(true); + }); + + it("loadGraph reads back the saved graph", () => { + const graph = makeGraph(); + saveGraph(testDir, graph); + const loaded = loadGraph(testDir); + + expect(loaded).not.toBeNull(); + expect(loaded!.project.name).toBe("test"); + expect(loaded!.nodes).toHaveLength(1); + }); + + it("loadGraph returns null when no graph exists", () => { + const loaded = loadGraph(testDir); + expect(loaded).toBeNull(); + }); + + it("saveMeta writes meta.json", () => { + const meta: AnalysisMeta = { + lastAnalyzedAt: new Date().toISOString(), + gitCommitHash: "abc123", + version: "1.0.0", + analyzedFiles: 5, + }; + saveMeta(testDir, meta); + + const filePath = join(testDir, ".understand-anything", "meta.json"); + expect(existsSync(filePath)).toBe(true); + }); + + it("loadMeta reads back saved meta", () => { + const meta: AnalysisMeta = { + lastAnalyzedAt: new Date().toISOString(), + gitCommitHash: "def456", + version: "1.0.0", + analyzedFiles: 10, + }; + saveMeta(testDir, meta); + const loaded = loadMeta(testDir); + + expect(loaded).not.toBeNull(); + expect(loaded!.gitCommitHash).toBe("def456"); + expect(loaded!.analyzedFiles).toBe(10); + }); + + it("loadMeta returns null when no meta exists", () => { + const loaded = loadMeta(testDir); + expect(loaded).toBeNull(); + }); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `pnpm --filter @understand-anything/core test` +Expected: FAIL — module `./index.js` not found + +**Step 3: Implement persistence module** + +Create: `packages/core/src/persistence/index.ts` + +```typescript +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import type { KnowledgeGraph, AnalysisMeta } from "../types.js"; + +const UA_DIR = ".understand-anything"; +const GRAPH_FILE = "knowledge-graph.json"; +const META_FILE = "meta.json"; + +function ensureDir(projectRoot: string): string { + const dir = join(projectRoot, UA_DIR); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + return dir; +} + +export function saveGraph(projectRoot: string, graph: KnowledgeGraph): void { + const dir = ensureDir(projectRoot); + const filePath = join(dir, GRAPH_FILE); + writeFileSync(filePath, JSON.stringify(graph, null, 2), "utf-8"); +} + +export function loadGraph(projectRoot: string): KnowledgeGraph | null { + const filePath = join(projectRoot, UA_DIR, GRAPH_FILE); + if (!existsSync(filePath)) return null; + const content = readFileSync(filePath, "utf-8"); + return JSON.parse(content) as KnowledgeGraph; +} + +export function saveMeta(projectRoot: string, meta: AnalysisMeta): void { + const dir = ensureDir(projectRoot); + const filePath = join(dir, META_FILE); + writeFileSync(filePath, JSON.stringify(meta, null, 2), "utf-8"); +} + +export function loadMeta(projectRoot: string): AnalysisMeta | null { + const filePath = join(projectRoot, UA_DIR, META_FILE); + if (!existsSync(filePath)) return null; + const content = readFileSync(filePath, "utf-8"); + return JSON.parse(content) as AnalysisMeta; +} +``` + +**Step 4: Update packages/core/src/index.ts** + +```typescript +export * from "./types.js"; +export * from "./persistence/index.js"; +``` + +**Step 5: Run tests** + +Run: `pnpm --filter @understand-anything/core test` +Expected: All 6 persistence tests PASS + 3 type tests PASS = 9 total + +**Step 6: Commit** + +```bash +git add packages/core/src/persistence/ packages/core/src/index.ts +git commit -m "feat(core): add JSON persistence for knowledge graph and meta" +``` + +--- + +## Task 4: Core Package — Tree-sitter Analyzer Plugin + +**Files:** +- Create: `packages/core/src/plugins/tree-sitter-plugin.ts` +- Create: `packages/core/src/plugins/tree-sitter-plugin.test.ts` + +**Step 1: Install tree-sitter dependencies** + +Run: `pnpm --filter @understand-anything/core add tree-sitter tree-sitter-javascript tree-sitter-typescript` +Expected: packages installed + +**Step 2: Write the failing test** + +Create: `packages/core/src/plugins/tree-sitter-plugin.test.ts` + +```typescript +import { describe, it, expect } from "vitest"; +import { TreeSitterPlugin } from "./tree-sitter-plugin.js"; + +describe("TreeSitterPlugin", () => { + const plugin = new TreeSitterPlugin(); + + describe("analyzeFile — TypeScript", () => { + const tsCode = ` +import { Request, Response } from "express"; +import { db } from "../db/connection"; + +export function handleLogin(req: Request, res: Response): void { + const { email, password } = req.body; + validateCredentials(email, password); +} + +function validateCredentials(email: string, password: string): boolean { + return email.length > 0 && password.length > 0; +} + +export class AuthService { + private secret: string; + + constructor(secret: string) { + this.secret = secret; + } + + verify(token: string): boolean { + return token.length > 0; + } + + refresh(token: string): string { + return token; + } +} +`; + + it("extracts function declarations", () => { + const result = plugin.analyzeFile("src/auth.ts", tsCode); + const funcNames = result.functions.map((f) => f.name); + expect(funcNames).toContain("handleLogin"); + expect(funcNames).toContain("validateCredentials"); + }); + + it("extracts class declarations with methods", () => { + const result = plugin.analyzeFile("src/auth.ts", tsCode); + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("AuthService"); + expect(result.classes[0].methods).toContain("verify"); + expect(result.classes[0].methods).toContain("refresh"); + }); + + it("extracts import statements", () => { + const result = plugin.analyzeFile("src/auth.ts", tsCode); + const sources = result.imports.map((i) => i.source); + expect(sources).toContain("express"); + expect(sources).toContain("../db/connection"); + }); + + it("extracts export names", () => { + const result = plugin.analyzeFile("src/auth.ts", tsCode); + const exportNames = result.exports.map((e) => e.name); + expect(exportNames).toContain("handleLogin"); + expect(exportNames).toContain("AuthService"); + }); + }); + + describe("analyzeFile — JavaScript", () => { + const jsCode = ` +const express = require("express"); + +function middleware(req, res, next) { + next(); +} + +module.exports = { middleware }; +`; + + it("extracts functions from JavaScript", () => { + const result = plugin.analyzeFile("src/app.js", jsCode); + const funcNames = result.functions.map((f) => f.name); + expect(funcNames).toContain("middleware"); + }); + }); + + describe("resolveImports", () => { + const code = ` +import { foo } from "./utils"; +import bar from "../lib/bar"; +import * as path from "path"; +`; + + it("resolves relative import paths", () => { + const imports = plugin.resolveImports("src/index.ts", code); + const paths = imports.map((i) => i.source); + expect(paths).toContain("./utils"); + expect(paths).toContain("../lib/bar"); + expect(paths).toContain("path"); + }); + }); + + describe("languages", () => { + it("supports typescript and javascript", () => { + expect(plugin.languages).toContain("typescript"); + expect(plugin.languages).toContain("javascript"); + }); + }); +}); +``` + +**Step 3: Run test to verify it fails** + +Run: `pnpm --filter @understand-anything/core test` +Expected: FAIL — module not found + +**Step 4: Implement the tree-sitter plugin** + +Create: `packages/core/src/plugins/tree-sitter-plugin.ts` + +```typescript +import Parser from "tree-sitter"; +import TypeScript from "tree-sitter-typescript"; +import JavaScript from "tree-sitter-javascript"; +import type { + AnalyzerPlugin, + StructuralAnalysis, + ImportResolution, + CallGraphEntry, +} from "../types.js"; + +const tsParser = new Parser(); +tsParser.setLanguage(TypeScript.typescript); + +const jsParser = new Parser(); +jsParser.setLanguage(JavaScript); + +function getParser(filePath: string): Parser { + if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return tsParser; + return jsParser; +} + +function traverse( + node: Parser.SyntaxNode, + callback: (node: Parser.SyntaxNode) => void, +): void { + callback(node); + for (let i = 0; i < node.childCount; i++) { + traverse(node.child(i)!, callback); + } +} + +export class TreeSitterPlugin implements AnalyzerPlugin { + name = "tree-sitter"; + languages = ["typescript", "javascript"]; + + analyzeFile(filePath: string, content: string): StructuralAnalysis { + const parser = getParser(filePath); + const tree = parser.parse(content); + const root = tree.rootNode; + + const functions: StructuralAnalysis["functions"] = []; + const classes: StructuralAnalysis["classes"] = []; + const imports: StructuralAnalysis["imports"] = []; + const exports: StructuralAnalysis["exports"] = []; + + traverse(root, (node) => { + // Function declarations + if ( + node.type === "function_declaration" || + node.type === "function_signature" + ) { + const nameNode = node.childByFieldName("name"); + if (nameNode) { + functions.push({ + name: nameNode.text, + lineRange: [node.startPosition.row + 1, node.endPosition.row + 1], + params: this.extractParams(node), + }); + } + } + + // Arrow functions assigned to variables + if ( + node.type === "lexical_declaration" || + node.type === "variable_declaration" + ) { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child?.type === "variable_declarator") { + const nameNode = child.childByFieldName("name"); + const valueNode = child.childByFieldName("value"); + if (nameNode && valueNode?.type === "arrow_function") { + functions.push({ + name: nameNode.text, + lineRange: [ + node.startPosition.row + 1, + node.endPosition.row + 1, + ], + params: this.extractParams(valueNode), + }); + } + } + } + } + + // Class declarations + if (node.type === "class_declaration") { + const nameNode = node.childByFieldName("name"); + const bodyNode = node.childByFieldName("body"); + if (nameNode && bodyNode) { + const methods: string[] = []; + const properties: string[] = []; + + for (let i = 0; i < bodyNode.childCount; i++) { + const member = bodyNode.child(i); + if (member?.type === "method_definition") { + const methodName = member.childByFieldName("name"); + if (methodName && methodName.text !== "constructor") { + methods.push(methodName.text); + } + } + if ( + member?.type === "public_field_definition" || + member?.type === "property_definition" + ) { + const propName = member.childByFieldName("name"); + if (propName) properties.push(propName.text); + } + } + + classes.push({ + name: nameNode.text, + lineRange: [ + node.startPosition.row + 1, + node.endPosition.row + 1, + ], + methods, + properties, + }); + } + } + + // Import statements + if (node.type === "import_statement") { + const sourceNode = node.children.find( + (c) => c.type === "string" || c.type === "string_fragment", + ); + let source = ""; + if (sourceNode) { + source = sourceNode.text.replace(/['"]/g, ""); + } + // Try to find string_fragment inside string + if (!source) { + traverse(node, (child) => { + if (child.type === "string_fragment" || child.type === "string_content") { + source = child.text; + } + }); + } + + const specifiers: string[] = []; + traverse(node, (child) => { + if (child.type === "import_specifier") { + const nameChild = child.childByFieldName("name"); + if (nameChild) specifiers.push(nameChild.text); + } + if (child.type === "identifier" && child.parent?.type === "import_clause") { + specifiers.push(child.text); + } + if (child.type === "namespace_import") { + const nameChild = child.children.find((c) => c.type === "identifier"); + if (nameChild) specifiers.push(`* as ${nameChild.text}`); + } + }); + + if (source) { + imports.push({ + source, + specifiers, + lineNumber: node.startPosition.row + 1, + }); + } + } + + // Export statements + if (node.type === "export_statement") { + // export function / export class + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if ( + child?.type === "function_declaration" || + child?.type === "class_declaration" + ) { + const nameNode = child.childByFieldName("name"); + if (nameNode) { + exports.push({ + name: nameNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + if (child?.type === "lexical_declaration") { + traverse(child, (grandchild) => { + if (grandchild.type === "variable_declarator") { + const nameNode = grandchild.childByFieldName("name"); + if (nameNode) { + exports.push({ + name: nameNode.text, + lineNumber: node.startPosition.row + 1, + }); + } + } + }); + } + } + } + }); + + return { functions, classes, imports, exports }; + } + + resolveImports(filePath: string, content: string): ImportResolution[] { + const analysis = this.analyzeFile(filePath, content); + return analysis.imports.map((imp) => ({ + source: imp.source, + resolvedPath: imp.source, // Basic — full resolution needs fs access + specifiers: imp.specifiers, + })); + } + + extractCallGraph(filePath: string, content: string): CallGraphEntry[] { + const parser = getParser(filePath); + const tree = parser.parse(content); + const entries: CallGraphEntry[] = []; + + // Find all function scopes and call expressions within them + const functionScopes: Array<{ + name: string; + node: Parser.SyntaxNode; + }> = []; + + traverse(tree.rootNode, (node) => { + if (node.type === "function_declaration") { + const nameNode = node.childByFieldName("name"); + if (nameNode) { + functionScopes.push({ name: nameNode.text, node }); + } + } + }); + + for (const scope of functionScopes) { + traverse(scope.node, (node) => { + if (node.type === "call_expression") { + const funcNode = node.childByFieldName("function"); + if (funcNode) { + const callee = + funcNode.type === "member_expression" + ? funcNode.text + : funcNode.text; + entries.push({ + caller: scope.name, + callee, + lineNumber: node.startPosition.row + 1, + }); + } + } + }); + } + + return entries; + } + + private extractParams(node: Parser.SyntaxNode): string[] { + const params: string[] = []; + const paramsNode = node.childByFieldName("parameters"); + if (paramsNode) { + for (let i = 0; i < paramsNode.childCount; i++) { + const param = paramsNode.child(i); + if ( + param && + param.type !== "," && + param.type !== "(" && + param.type !== ")" + ) { + const nameNode = param.childByFieldName("name") || + param.childByFieldName("pattern"); + if (nameNode) params.push(nameNode.text); + else if (param.type === "identifier") params.push(param.text); + } + } + } + return params; + } +} +``` + +**Step 5: Update packages/core/src/index.ts** + +```typescript +export * from "./types.js"; +export * from "./persistence/index.js"; +export { TreeSitterPlugin } from "./plugins/tree-sitter-plugin.js"; +``` + +**Step 6: Run tests** + +Run: `pnpm --filter @understand-anything/core test` +Expected: All tree-sitter tests PASS. Some tests may need adjustment based on exact tree-sitter parse output — iterate until green. + +**Step 7: Commit** + +```bash +git add packages/core/src/plugins/ packages/core/src/index.ts packages/core/package.json pnpm-lock.yaml +git commit -m "feat(core): add tree-sitter analyzer plugin for TS/JS" +``` + +--- + +## Task 5: Core Package — LLM Analysis Engine + +**Files:** +- Create: `packages/core/src/analyzer/llm-analyzer.ts` +- Create: `packages/core/src/analyzer/llm-analyzer.test.ts` +- Create: `packages/core/src/analyzer/graph-builder.ts` +- Create: `packages/core/src/analyzer/graph-builder.test.ts` + +**Step 1: Write the graph builder test** + +The graph builder takes structural analysis + LLM summaries and assembles a KnowledgeGraph. + +Create: `packages/core/src/analyzer/graph-builder.test.ts` + +```typescript +import { describe, it, expect } from "vitest"; +import { GraphBuilder } from "./graph-builder.js"; +import type { StructuralAnalysis } from "../types.js"; + +describe("GraphBuilder", () => { + it("creates file nodes from file list", () => { + const builder = new GraphBuilder("test-project", "abc123"); + + builder.addFile("src/index.ts", { + summary: "Application entry point", + tags: ["entry", "main"], + complexity: "simple" as const, + }); + + const graph = builder.build(); + expect(graph.nodes).toHaveLength(1); + expect(graph.nodes[0].type).toBe("file"); + expect(graph.nodes[0].name).toBe("index.ts"); + expect(graph.nodes[0].filePath).toBe("src/index.ts"); + }); + + it("creates function nodes from structural analysis", () => { + const builder = new GraphBuilder("test-project", "abc123"); + const analysis: StructuralAnalysis = { + functions: [ + { name: "handleLogin", lineRange: [5, 15], params: ["req", "res"] }, + ], + classes: [], + imports: [], + exports: [], + }; + + builder.addFileWithAnalysis("src/auth.ts", analysis, { + summaries: { handleLogin: "Handles user login" }, + fileSummary: "Authentication module", + tags: ["auth"], + complexity: "moderate" as const, + }); + + const graph = builder.build(); + const funcNodes = graph.nodes.filter((n) => n.type === "function"); + expect(funcNodes).toHaveLength(1); + expect(funcNodes[0].name).toBe("handleLogin"); + expect(funcNodes[0].summary).toBe("Handles user login"); + }); + + it("creates contains edges between files and their functions", () => { + const builder = new GraphBuilder("test-project", "abc123"); + const analysis: StructuralAnalysis = { + functions: [ + { name: "foo", lineRange: [1, 5], params: [] }, + ], + classes: [], + imports: [], + exports: [], + }; + + builder.addFileWithAnalysis("src/utils.ts", analysis, { + summaries: { foo: "A utility function" }, + fileSummary: "Utility functions", + tags: ["utils"], + complexity: "simple" as const, + }); + + const graph = builder.build(); + const containsEdges = graph.edges.filter((e) => e.type === "contains"); + expect(containsEdges).toHaveLength(1); + expect(containsEdges[0].direction).toBe("forward"); + }); + + it("creates import edges from structural analysis", () => { + const builder = new GraphBuilder("test-project", "abc123"); + + builder.addFile("src/index.ts", { + summary: "Entry", + tags: [], + complexity: "simple" as const, + }); + builder.addFile("src/utils.ts", { + summary: "Utils", + tags: [], + complexity: "simple" as const, + }); + + builder.addImportEdge("src/index.ts", "src/utils.ts"); + + const graph = builder.build(); + const importEdges = graph.edges.filter((e) => e.type === "imports"); + expect(importEdges).toHaveLength(1); + }); + + it("sets project metadata correctly", () => { + const builder = new GraphBuilder("my-project", "def456"); + const graph = builder.build(); + + expect(graph.project.name).toBe("my-project"); + expect(graph.project.gitCommitHash).toBe("def456"); + expect(graph.version).toBe("1.0.0"); + }); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `pnpm --filter @understand-anything/core test` +Expected: FAIL — module not found + +**Step 3: Implement GraphBuilder** + +Create: `packages/core/src/analyzer/graph-builder.ts` + +```typescript +import type { + KnowledgeGraph, + GraphNode, + GraphEdge, + StructuralAnalysis, +} from "../types.js"; + +interface FileMeta { + summary: string; + tags: string[]; + complexity: "simple" | "moderate" | "complex"; +} + +interface FileAnalysisMeta extends FileMeta { + summaries: Record; // function/class name -> summary + fileSummary: string; +} + +function fileId(filePath: string): string { + return `file:${filePath}`; +} + +function funcId(filePath: string, funcName: string): string { + return `func:${filePath}:${funcName}`; +} + +function classId(filePath: string, className: string): string { + return `class:${filePath}:${className}`; +} + +export class GraphBuilder { + private nodes: GraphNode[] = []; + private edges: GraphEdge[] = []; + private projectName: string; + private gitHash: string; + private languages: Set = new Set(); + + constructor(projectName: string, gitHash: string) { + this.projectName = projectName; + this.gitHash = gitHash; + } + + addFile(filePath: string, meta: FileMeta): void { + const ext = filePath.split(".").pop() || ""; + this.detectLanguage(ext); + + const name = filePath.split("/").pop() || filePath; + this.nodes.push({ + id: fileId(filePath), + type: "file", + name, + filePath, + summary: meta.summary, + tags: meta.tags, + complexity: meta.complexity, + }); + } + + addFileWithAnalysis( + filePath: string, + analysis: StructuralAnalysis, + meta: FileAnalysisMeta, + ): void { + // Add the file node + this.addFile(filePath, { + summary: meta.fileSummary, + tags: meta.tags, + complexity: meta.complexity, + }); + + const fId = fileId(filePath); + + // Add function nodes + for (const func of analysis.functions) { + const id = funcId(filePath, func.name); + this.nodes.push({ + id, + type: "function", + name: func.name, + filePath, + lineRange: func.lineRange, + summary: meta.summaries[func.name] || `Function ${func.name}`, + tags: meta.tags, + complexity: meta.complexity, + }); + + // File contains function + this.edges.push({ + source: fId, + target: id, + type: "contains", + direction: "forward", + weight: 1.0, + }); + } + + // Add class nodes + for (const cls of analysis.classes) { + const id = classId(filePath, cls.name); + this.nodes.push({ + id, + type: "class", + name: cls.name, + filePath, + lineRange: cls.lineRange, + summary: meta.summaries[cls.name] || `Class ${cls.name}`, + tags: meta.tags, + complexity: meta.complexity, + }); + + // File contains class + this.edges.push({ + source: fId, + target: id, + type: "contains", + direction: "forward", + weight: 1.0, + }); + } + } + + addImportEdge(fromFile: string, toFile: string): void { + this.edges.push({ + source: fileId(fromFile), + target: fileId(toFile), + type: "imports", + direction: "forward", + weight: 0.7, + }); + } + + addCallEdge( + callerFile: string, + callerFunc: string, + calleeFile: string, + calleeFunc: string, + ): void { + this.edges.push({ + source: funcId(callerFile, callerFunc), + target: funcId(calleeFile, calleeFunc), + type: "calls", + direction: "forward", + weight: 0.8, + }); + } + + build(): KnowledgeGraph { + return { + version: "1.0.0", + project: { + name: this.projectName, + languages: Array.from(this.languages), + frameworks: [], + description: "", + analyzedAt: new Date().toISOString(), + gitCommitHash: this.gitHash, + }, + nodes: this.nodes, + edges: this.edges, + layers: [], + tour: [], + }; + } + + private detectLanguage(ext: string): void { + const langMap: Record = { + ts: "typescript", + tsx: "typescript", + js: "javascript", + jsx: "javascript", + py: "python", + go: "go", + rs: "rust", + java: "java", + c: "c", + cpp: "cpp", + h: "c", + }; + if (langMap[ext]) this.languages.add(langMap[ext]); + } +} +``` + +**Step 4: Run tests** + +Run: `pnpm --filter @understand-anything/core test` +Expected: All GraphBuilder tests PASS + +**Step 5: Create the LLM analyzer interface** + +Create: `packages/core/src/analyzer/llm-analyzer.ts` + +This defines the interface for LLM-based analysis. The actual LLM calls happen via the skill (which has access to the Claude session). The core package defines the prompts and expected response format. + +```typescript +/** + * LLM Analyzer — defines prompts and response parsing for LLM-based code analysis. + * + * The actual LLM invocation is handled by the caller (skill or dashboard with API key). + * This module provides the prompt templates and response parsers. + */ + +export interface LLMFileAnalysis { + fileSummary: string; + tags: string[]; + complexity: "simple" | "moderate" | "complex"; + functionSummaries: Record; + classSummaries: Record; + languageNotes?: string; +} + +export interface LLMProjectSummary { + description: string; + frameworks: string[]; + layers: Array<{ + name: string; + description: string; + filePatterns: string[]; + }>; +} + +/** + * Generates the prompt for analyzing a single file. + */ +export function buildFileAnalysisPrompt( + filePath: string, + content: string, + projectContext: string, +): string { + return `You are analyzing a source code file as part of a codebase understanding tool. + +Project context: ${projectContext} + +File: ${filePath} + +\`\`\` +${content} +\`\`\` + +Analyze this file and respond with ONLY valid JSON (no markdown, no explanation): + +{ + "fileSummary": "One-sentence plain-English description of what this file does", + "tags": ["tag1", "tag2"], + "complexity": "simple|moderate|complex", + "functionSummaries": { + "functionName": "What this function does in plain English" + }, + "classSummaries": { + "className": "What this class does in plain English" + }, + "languageNotes": "Optional: any language-specific patterns worth noting for someone unfamiliar with this language" +}`; +} + +/** + * Generates the prompt for a project-level summary. + */ +export function buildProjectSummaryPrompt( + fileList: string[], + sampleFiles: Array<{ path: string; content: string }>, +): string { + const fileListStr = fileList.map((f) => ` - ${f}`).join("\n"); + const sampleStr = sampleFiles + .map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 500)}\n\`\`\``) + .join("\n\n"); + + return `You are analyzing a software project to generate a high-level understanding. + +File list: +${fileListStr} + +Sample files: +${sampleStr} + +Analyze this project and respond with ONLY valid JSON: + +{ + "description": "2-3 sentence description of what this project does", + "frameworks": ["framework1", "library1"], + "layers": [ + { + "name": "Layer Name", + "description": "What this layer handles", + "filePatterns": ["src/api/**", "src/routes/**"] + } + ] +}`; +} + +/** + * Parses the LLM response for file analysis. Handles common LLM output issues. + */ +export function parseFileAnalysisResponse( + response: string, +): LLMFileAnalysis | null { + try { + // Strip markdown code fences if present + let cleaned = response.trim(); + if (cleaned.startsWith("```")) { + cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, ""); + } + const parsed = JSON.parse(cleaned); + + return { + fileSummary: parsed.fileSummary || "No summary available", + tags: Array.isArray(parsed.tags) ? parsed.tags : [], + complexity: ["simple", "moderate", "complex"].includes(parsed.complexity) + ? parsed.complexity + : "moderate", + functionSummaries: parsed.functionSummaries || {}, + classSummaries: parsed.classSummaries || {}, + languageNotes: parsed.languageNotes, + }; + } catch { + return null; + } +} + +/** + * Parses the LLM response for project summary. + */ +export function parseProjectSummaryResponse( + response: string, +): LLMProjectSummary | null { + try { + let cleaned = response.trim(); + if (cleaned.startsWith("```")) { + cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, ""); + } + const parsed = JSON.parse(cleaned); + + return { + description: parsed.description || "", + frameworks: Array.isArray(parsed.frameworks) ? parsed.frameworks : [], + layers: Array.isArray(parsed.layers) ? parsed.layers : [], + }; + } catch { + return null; + } +} +``` + +**Step 6: Write tests for LLM analyzer** + +Create: `packages/core/src/analyzer/llm-analyzer.test.ts` + +```typescript +import { describe, it, expect } from "vitest"; +import { + buildFileAnalysisPrompt, + parseFileAnalysisResponse, + buildProjectSummaryPrompt, + parseProjectSummaryResponse, +} from "./llm-analyzer.js"; + +describe("LLM Analyzer", () => { + describe("buildFileAnalysisPrompt", () => { + it("includes file path and content", () => { + const prompt = buildFileAnalysisPrompt( + "src/auth.ts", + "function login() {}", + "A web app", + ); + expect(prompt).toContain("src/auth.ts"); + expect(prompt).toContain("function login() {}"); + expect(prompt).toContain("A web app"); + }); + }); + + describe("parseFileAnalysisResponse", () => { + it("parses valid JSON response", () => { + const response = JSON.stringify({ + fileSummary: "Handles authentication", + tags: ["auth", "login"], + complexity: "moderate", + functionSummaries: { login: "Logs user in" }, + classSummaries: {}, + }); + + const result = parseFileAnalysisResponse(response); + expect(result).not.toBeNull(); + expect(result!.fileSummary).toBe("Handles authentication"); + expect(result!.tags).toContain("auth"); + }); + + it("handles markdown-wrapped JSON", () => { + const response = '```json\n{"fileSummary": "Test", "tags": [], "complexity": "simple", "functionSummaries": {}, "classSummaries": {}}\n```'; + + const result = parseFileAnalysisResponse(response); + expect(result).not.toBeNull(); + expect(result!.fileSummary).toBe("Test"); + }); + + it("returns null for invalid JSON", () => { + const result = parseFileAnalysisResponse("not json at all"); + expect(result).toBeNull(); + }); + + it("defaults complexity to moderate for unknown values", () => { + const response = JSON.stringify({ + fileSummary: "Test", + tags: [], + complexity: "unknown", + functionSummaries: {}, + classSummaries: {}, + }); + + const result = parseFileAnalysisResponse(response); + expect(result!.complexity).toBe("moderate"); + }); + }); + + describe("buildProjectSummaryPrompt", () => { + it("includes file list", () => { + const prompt = buildProjectSummaryPrompt( + ["src/index.ts", "src/auth.ts"], + [{ path: "src/index.ts", content: "console.log('hi')" }], + ); + expect(prompt).toContain("src/index.ts"); + expect(prompt).toContain("src/auth.ts"); + }); + }); + + describe("parseProjectSummaryResponse", () => { + it("parses valid response", () => { + const response = JSON.stringify({ + description: "A REST API", + frameworks: ["express"], + layers: [{ name: "API", description: "HTTP layer", filePatterns: ["src/routes/**"] }], + }); + + const result = parseProjectSummaryResponse(response); + expect(result).not.toBeNull(); + expect(result!.frameworks).toContain("express"); + expect(result!.layers).toHaveLength(1); + }); + }); +}); +``` + +**Step 7: Update packages/core/src/index.ts** + +```typescript +export * from "./types.js"; +export * from "./persistence/index.js"; +export { TreeSitterPlugin } from "./plugins/tree-sitter-plugin.js"; +export { GraphBuilder } from "./analyzer/graph-builder.js"; +export { + buildFileAnalysisPrompt, + buildProjectSummaryPrompt, + parseFileAnalysisResponse, + parseProjectSummaryResponse, +} from "./analyzer/llm-analyzer.js"; +export type { + LLMFileAnalysis, + LLMProjectSummary, +} from "./analyzer/llm-analyzer.js"; +``` + +**Step 8: Run all tests** + +Run: `pnpm --filter @understand-anything/core test` +Expected: All tests PASS + +**Step 9: Commit** + +```bash +git add packages/core/src/analyzer/ packages/core/src/index.ts +git commit -m "feat(core): add graph builder and LLM analysis prompt system" +``` + +--- + +## Task 6: Dashboard Package — Scaffolding with Vite + React + +**Files:** +- Create: `packages/dashboard/` (via Vite scaffold, then customize) + +**Step 1: Scaffold React app with Vite** + +Run: `cd packages && pnpm create vite dashboard --template react-ts` +Then: Remove boilerplate (App.css, etc.), keep structure. + +**Step 2: Install dashboard dependencies** + +Run: `cd packages/dashboard && pnpm add @xyflow/react @monaco-editor/react zustand && pnpm add -D tailwindcss @tailwindcss/vite` + +**Step 3: Configure TailwindCSS** + +Update `packages/dashboard/vite.config.ts`: + +```typescript +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], +}); +``` + +Replace `packages/dashboard/src/index.css`: + +```css +@import "tailwindcss"; +``` + +**Step 4: Add workspace dependency on core** + +Add to `packages/dashboard/package.json` dependencies: + +```json +"@understand-anything/core": "workspace:*" +``` + +Run: `pnpm install` + +**Step 5: Create the Zustand store** + +Create: `packages/dashboard/src/store.ts` + +```typescript +import { create } from "zustand"; +import type { KnowledgeGraph, GraphNode } from "@understand-anything/core"; + +interface DashboardStore { + graph: KnowledgeGraph | null; + selectedNodeId: string | null; + searchQuery: string; + searchResults: string[]; // node IDs + + setGraph: (graph: KnowledgeGraph) => void; + selectNode: (nodeId: string | null) => void; + setSearchQuery: (query: string) => void; +} + +export const useDashboardStore = create()((set, get) => ({ + graph: null, + selectedNodeId: null, + searchQuery: "", + searchResults: [], + + setGraph: (graph) => set({ graph }), + + selectNode: (nodeId) => set({ selectedNodeId: nodeId }), + + setSearchQuery: (query) => { + const graph = get().graph; + if (!graph || !query.trim()) { + set({ searchQuery: query, searchResults: [] }); + return; + } + + const lower = query.toLowerCase(); + const results = graph.nodes + .filter( + (node) => + node.name.toLowerCase().includes(lower) || + node.summary.toLowerCase().includes(lower) || + node.tags.some((tag) => tag.toLowerCase().includes(lower)), + ) + .map((n) => n.id); + + set({ searchQuery: query, searchResults: results }); + }, +})); +``` + +**Step 6: Commit** + +```bash +git add packages/dashboard/ +git commit -m "feat(dashboard): scaffold React + Vite app with Tailwind, Zustand, and core dependency" +``` + +--- + +## Task 7: Dashboard — Graph View Panel with React Flow + +**Files:** +- Create: `packages/dashboard/src/components/GraphView.tsx` +- Create: `packages/dashboard/src/components/CustomNode.tsx` + +**Step 1: Create the custom node component** + +Create: `packages/dashboard/src/components/CustomNode.tsx` + +```tsx +import { Handle, Position } from "@xyflow/react"; +import type { NodeProps } from "@xyflow/react"; + +interface CustomNodeData { + label: string; + nodeType: "file" | "function" | "class" | "module" | "concept"; + summary: string; + complexity: "simple" | "moderate" | "complex"; + isHighlighted: boolean; + isSelected: boolean; + [key: string]: unknown; +} + +const typeColors: Record = { + file: "bg-blue-900 border-blue-500", + function: "bg-green-900 border-green-500", + class: "bg-purple-900 border-purple-500", + module: "bg-orange-900 border-orange-500", + concept: "bg-pink-900 border-pink-500", +}; + +const complexityBadge: Record = { + simple: "bg-green-700 text-green-100", + moderate: "bg-yellow-700 text-yellow-100", + complex: "bg-red-700 text-red-100", +}; + +export function CustomNode({ data }: NodeProps) { + const colorClass = typeColors[data.nodeType] || "bg-gray-900 border-gray-500"; + const highlightClass = data.isHighlighted + ? "ring-2 ring-yellow-400 shadow-lg shadow-yellow-400/20" + : ""; + const selectedClass = data.isSelected + ? "ring-2 ring-white shadow-lg" + : ""; + + return ( +
+ + +
+ + {data.nodeType} + + + {data.complexity} + +
+ +
+ {data.label} +
+ +
+ {data.summary} +
+ + +
+ ); +} +``` + +**Step 2: Create the GraphView component** + +Create: `packages/dashboard/src/components/GraphView.tsx` + +```tsx +import { useCallback, useMemo } from "react"; +import { + ReactFlow, + useNodesState, + useEdgesState, + Background, + Controls, + MiniMap, +} from "@xyflow/react"; +import type { Node, Edge, NodeMouseHandler } from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import { useDashboardStore } from "../store"; +import { CustomNode } from "./CustomNode"; +import type { KnowledgeGraph } from "@understand-anything/core"; + +const nodeTypes = { custom: CustomNode }; + +function graphToReactFlow( + graph: KnowledgeGraph, + searchResults: string[], + selectedNodeId: string | null, +) { + const nodes: Node[] = graph.nodes.map((node, index) => ({ + id: node.id, + type: "custom", + position: { + x: (index % 5) * 280, + y: Math.floor(index / 5) * 160, + }, + data: { + label: node.name, + nodeType: node.type, + summary: node.summary, + complexity: node.complexity, + isHighlighted: searchResults.includes(node.id), + isSelected: node.id === selectedNodeId, + }, + })); + + const edges: Edge[] = graph.edges.map((edge, index) => ({ + id: `e-${index}`, + source: edge.source, + target: edge.target, + label: edge.type, + animated: edge.type === "calls", + style: { stroke: searchResults.length > 0 ? "#555" : "#888" }, + })); + + return { nodes, edges }; +} + +export function GraphView() { + const graph = useDashboardStore((s) => s.graph); + const searchResults = useDashboardStore((s) => s.searchResults); + const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); + const selectNode = useDashboardStore((s) => s.selectNode); + + const { initialNodes, initialEdges } = useMemo(() => { + if (!graph) return { initialNodes: [], initialEdges: [] }; + const { nodes, edges } = graphToReactFlow( + graph, + searchResults, + selectedNodeId, + ); + return { initialNodes: nodes, initialEdges: edges }; + }, [graph, searchResults, selectedNodeId]); + + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + const onNodeClick: NodeMouseHandler = useCallback( + (_event, node) => { + selectNode(node.id); + }, + [selectNode], + ); + + if (!graph) { + return ( +
+ No knowledge graph loaded. Run /understand first. +
+ ); + } + + return ( + + + + + + ); +} +``` + +**Step 3: Commit** + +```bash +git add packages/dashboard/src/components/ +git commit -m "feat(dashboard): add graph view with React Flow and custom nodes" +``` + +--- + +## Task 8: Dashboard — Code Viewer Panel with Monaco Editor + +**Files:** +- Create: `packages/dashboard/src/components/CodeViewer.tsx` + +**Step 1: Create the CodeViewer component** + +Create: `packages/dashboard/src/components/CodeViewer.tsx` + +```tsx +import Editor from "@monaco-editor/react"; +import { useDashboardStore } from "../store"; + +const extToLanguage: Record = { + ts: "typescript", + tsx: "typescript", + js: "javascript", + jsx: "javascript", + py: "python", + go: "go", + rs: "rust", + java: "java", + json: "json", + md: "markdown", + css: "css", + html: "html", +}; + +function detectLanguage(filePath: string): string { + const ext = filePath.split(".").pop() || ""; + return extToLanguage[ext] || "plaintext"; +} + +export function CodeViewer() { + const graph = useDashboardStore((s) => s.graph); + const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); + + const selectedNode = graph?.nodes.find((n) => n.id === selectedNodeId); + + if (!selectedNode || !selectedNode.filePath) { + return ( +
+ Select a node to view its source code. +
+ ); + } + + // In MVP, we show a placeholder message directing to file path + // Full implementation will load file content via API or embedded data + const placeholder = `// File: ${selectedNode.filePath} +// Lines: ${selectedNode.lineRange ? `${selectedNode.lineRange[0]}-${selectedNode.lineRange[1]}` : "all"} +// +// ${selectedNode.summary} +// +// To view the actual source code, the dashboard needs access to the project files. +// This will be connected in the next phase via a local API server.`; + + return ( +
+
+ + {selectedNode.type.toUpperCase()} + + {selectedNode.name} + + {selectedNode.filePath} + +
+
+ +
+
+ ); +} +``` + +**Step 2: Commit** + +```bash +git add packages/dashboard/src/components/CodeViewer.tsx +git commit -m "feat(dashboard): add code viewer panel with Monaco Editor" +``` + +--- + +## Task 9: Dashboard — Search Bar and Main Layout + +**Files:** +- Create: `packages/dashboard/src/components/SearchBar.tsx` +- Create: `packages/dashboard/src/components/NodeInfo.tsx` +- Modify: `packages/dashboard/src/App.tsx` + +**Step 1: Create SearchBar component** + +Create: `packages/dashboard/src/components/SearchBar.tsx` + +```tsx +import { useDashboardStore } from "../store"; + +export function SearchBar() { + const searchQuery = useDashboardStore((s) => s.searchQuery); + const searchResults = useDashboardStore((s) => s.searchResults); + const setSearchQuery = useDashboardStore((s) => s.setSearchQuery); + + return ( +
+ + + + setSearchQuery(e.target.value)} + placeholder='Search: "authentication", "api layer", "database"...' + className="flex-1 bg-transparent text-white placeholder-gray-500 outline-none text-sm" + /> + {searchQuery && ( + + {searchResults.length} results + + )} +
+ ); +} +``` + +**Step 2: Create NodeInfo panel (placeholder for chat + learn panels)** + +Create: `packages/dashboard/src/components/NodeInfo.tsx` + +```tsx +import { useDashboardStore } from "../store"; + +export function NodeInfo() { + const graph = useDashboardStore((s) => s.graph); + const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); + + const selectedNode = graph?.nodes.find((n) => n.id === selectedNodeId); + + if (!selectedNode) { + return ( +
+ Select a node to see details. Chat and Learn panels coming in Phase 2. +
+ ); + } + + // Find connected nodes + const connectedEdges = graph?.edges.filter( + (e) => e.source === selectedNodeId || e.target === selectedNodeId, + ) || []; + + return ( +
+

+ {selectedNode.name} +

+
+ + {selectedNode.type} + + + {selectedNode.complexity} + +
+ +

{selectedNode.summary}

+ + {selectedNode.languageNotes && ( +
+
Language Note
+

{selectedNode.languageNotes}

+
+ )} + + {selectedNode.tags.length > 0 && ( +
+
Tags
+
+ {selectedNode.tags.map((tag) => ( + + {tag} + + ))} +
+
+ )} + + {connectedEdges.length > 0 && ( +
+
+ Connections ({connectedEdges.length}) +
+
+ {connectedEdges.map((edge, i) => { + const otherId = + edge.source === selectedNodeId ? edge.target : edge.source; + const otherNode = graph?.nodes.find((n) => n.id === otherId); + const direction = + edge.source === selectedNodeId ? "\u2192" : "\u2190"; + return ( +
+ {direction} {edge.type}: {otherNode?.name || otherId} +
+ ); + })} +
+
+ )} +
+ ); +} +``` + +**Step 3: Create the main App layout** + +Replace: `packages/dashboard/src/App.tsx` + +```tsx +import { useEffect } from "react"; +import { SearchBar } from "./components/SearchBar"; +import { GraphView } from "./components/GraphView"; +import { CodeViewer } from "./components/CodeViewer"; +import { NodeInfo } from "./components/NodeInfo"; +import { useDashboardStore } from "./store"; +import type { KnowledgeGraph } from "@understand-anything/core"; + +// Load graph from a JSON file served at /knowledge-graph.json +// In production, this comes from .understand-anything/knowledge-graph.json +async function loadGraphData(): Promise { + try { + const response = await fetch("/knowledge-graph.json"); + if (!response.ok) return null; + return (await response.json()) as KnowledgeGraph; + } catch { + return null; + } +} + +function App() { + const setGraph = useDashboardStore((s) => s.setGraph); + const graph = useDashboardStore((s) => s.graph); + + useEffect(() => { + loadGraphData().then((g) => { + if (g) setGraph(g); + }); + }, [setGraph]); + + return ( +
+ {/* Header */} +
+

+ UNDERSTAND ANYTHING +

+ {graph && ( + + {graph.project.name} · {graph.nodes.length} nodes ·{" "} + {graph.edges.length} edges + + )} +
+ + {/* Search */} + + + {/* Main workspace: 2x2 grid */} +
+ {/* Top-left: Graph View */} +
+ +
+ + {/* Top-right: Code Viewer */} +
+ +
+ + {/* Bottom-left: Chat Panel (placeholder) */} +
+ Chat panel — coming in Phase 2 +
+ + {/* Bottom-right: Node Info / Learn Panel */} +
+ +
+
+
+ ); +} + +export default App; +``` + +**Step 4: Run the dashboard** + +Run: `pnpm --filter @understand-anything/dashboard dev` +Expected: Vite dev server starts at localhost:5173. Dashboard renders with "No knowledge graph loaded" message. + +**Step 5: Test with sample data** + +Create `packages/dashboard/public/knowledge-graph.json` with a sample graph for testing: + +```json +{ + "version": "1.0.0", + "project": { + "name": "sample-project", + "languages": ["typescript"], + "frameworks": ["express"], + "description": "A sample Express API", + "analyzedAt": "2026-03-14T00:00:00.000Z", + "gitCommitHash": "abc123" + }, + "nodes": [ + { + "id": "file:src/index.ts", + "type": "file", + "name": "index.ts", + "filePath": "src/index.ts", + "summary": "Application entry point, starts the Express server", + "tags": ["entry", "server"], + "complexity": "simple" + }, + { + "id": "file:src/auth/login.ts", + "type": "file", + "name": "login.ts", + "filePath": "src/auth/login.ts", + "summary": "Handles user authentication and login flow", + "tags": ["auth", "login"], + "complexity": "moderate" + }, + { + "id": "func:src/auth/login.ts:handleLogin", + "type": "function", + "name": "handleLogin", + "filePath": "src/auth/login.ts", + "lineRange": [10, 35], + "summary": "Validates credentials and returns a JWT token", + "tags": ["auth", "jwt"], + "complexity": "moderate" + }, + { + "id": "func:src/auth/login.ts:validateEmail", + "type": "function", + "name": "validateEmail", + "filePath": "src/auth/login.ts", + "lineRange": [37, 42], + "summary": "Checks if an email address is valid using regex", + "tags": ["validation", "email"], + "complexity": "simple" + }, + { + "id": "file:src/db/connection.ts", + "type": "file", + "name": "connection.ts", + "filePath": "src/db/connection.ts", + "summary": "Database connection pool using PostgreSQL", + "tags": ["database", "postgres"], + "complexity": "moderate" + } + ], + "edges": [ + { + "source": "file:src/index.ts", + "target": "file:src/auth/login.ts", + "type": "imports", + "direction": "forward", + "weight": 0.7 + }, + { + "source": "file:src/auth/login.ts", + "target": "func:src/auth/login.ts:handleLogin", + "type": "contains", + "direction": "forward", + "weight": 1.0 + }, + { + "source": "file:src/auth/login.ts", + "target": "func:src/auth/login.ts:validateEmail", + "type": "contains", + "direction": "forward", + "weight": 1.0 + }, + { + "source": "func:src/auth/login.ts:handleLogin", + "target": "func:src/auth/login.ts:validateEmail", + "type": "calls", + "direction": "forward", + "description": "handleLogin calls validateEmail to check the email format", + "weight": 0.8 + }, + { + "source": "file:src/auth/login.ts", + "target": "file:src/db/connection.ts", + "type": "imports", + "direction": "forward", + "weight": 0.7 + } + ], + "layers": [], + "tour": [] +} +``` + +**Step 6: Verify in browser** + +Open: `http://localhost:5173` +Expected: Dashboard loads, graph renders 5 nodes with edges, clicking a node shows details in the info panel and placeholder in code viewer. Search works. + +**Step 7: Commit** + +```bash +git add packages/dashboard/ +git commit -m "feat(dashboard): add main layout with search bar, graph view, code viewer, and node info panels" +``` + +--- + +## Task 10: Integration — Wire Everything Together + README + +**Files:** +- Create: `README.md` +- Create: `CLAUDE.md` + +**Step 1: Create README.md** + +```markdown +# Understand Anything + +An open-source tool that combines LLM intelligence with static analysis to help anyone understand any codebase — from junior developers to product managers. + +## Features + +- **Knowledge Graph** — Automatically maps your codebase into an interactive graph of files, functions, classes, and their relationships +- **Multi-Panel Dashboard** — Graph view, code viewer, chat, and learn panels in a workspace layout +- **Natural Language Search** — Search your codebase with plain English: "which parts handle authentication?" +- **Tree-sitter Analysis** — Accurate structural analysis for TypeScript, JavaScript (more languages coming) +- **LLM-Powered Summaries** — Every node gets a plain-English description of what it does and why + +## Quick Start + +```bash +# Install dependencies +pnpm install + +# Build the core package +pnpm --filter @understand-anything/core build + +# Start the dashboard dev server +pnpm dev:dashboard +``` + +## Project Structure + +``` +packages/ + core/ — Analysis engine: types, persistence, tree-sitter, LLM prompts + dashboard/ — React + TypeScript web dashboard + skill/ — Claude Code skill (coming soon) +``` + +## Tech Stack + +- TypeScript, pnpm workspaces +- React 18, Vite, TailwindCSS +- React Flow (graph visualization) +- Monaco Editor (code viewer) +- Zustand (state management) +- tree-sitter (static analysis) + +## License + +MIT +``` + +**Step 2: Create CLAUDE.md** + +```markdown +# Understand Anything + +## Project Overview +An open-source tool combining LLM intelligence + static analysis to produce interactive dashboards for understanding codebases. + +## Architecture +- **Monorepo** with pnpm workspaces +- **packages/core** — Shared analysis engine (types, persistence, tree-sitter plugin, LLM prompt templates) +- **packages/dashboard** — React + TypeScript web dashboard (React Flow, Monaco Editor, Zustand, TailwindCSS) +- **packages/skill** — Claude Code skill (not yet implemented) + +## Key Commands +- `pnpm install` — Install all dependencies +- `pnpm --filter @understand-anything/core build` — Build the core package +- `pnpm --filter @understand-anything/core test` — Run core tests +- `pnpm dev:dashboard` — Start dashboard dev server + +## Conventions +- TypeScript strict mode everywhere +- Vitest for testing +- ESM modules (`"type": "module"`) +- Knowledge graph JSON lives in `.understand-anything/` directory of analyzed projects +``` + +**Step 3: Commit** + +```bash +git add README.md CLAUDE.md +git commit -m "docs: add README and CLAUDE.md with project overview and conventions" +``` + +--- + +## Verification Checklist + +After completing all 10 tasks: + +1. **`pnpm install`** — No errors +2. **`pnpm --filter @understand-anything/core build`** — Compiles clean +3. **`pnpm --filter @understand-anything/core test`** — All tests pass (types, persistence, tree-sitter, graph builder, LLM analyzer) +4. **`pnpm dev:dashboard`** — Dashboard starts at localhost:5173 +5. **Dashboard with sample data** — Loads `knowledge-graph.json`, graph renders, nodes clickable, search works, code viewer shows node info +6. **Git log** — Clean history with 10 logical commits diff --git a/docs/plans/2026-03-14-understand-anything-design.md b/docs/plans/2026-03-14-understand-anything-design.md new file mode 100644 index 0000000..f4885e7 --- /dev/null +++ b/docs/plans/2026-03-14-understand-anything-design.md @@ -0,0 +1,268 @@ +# Understand Anything — Design & Implementation Plan + +## Context + +AI coding tools have made writing code easy, but understanding code remains hard. Junior developers, non-programmers (PMs, designers), and even experienced devs working in unfamiliar languages struggle to comprehend codebases they didn't write — or that AI wrote for them. The only entity that "understands" the code is the AI itself. + +**Understand Anything** bridges this gap: an open-source tool that combines LLM intelligence with static analysis to produce an interactive, multi-persona dashboard for understanding any codebase. It runs as a Claude Code skill (leveraging the active session) and serves a rich web dashboard. + +--- + +## Architecture: Monorepo with Shared Core + +``` +understand-anything/ +├── packages/ +│ ├── core/ # Shared analysis engine +│ │ ├── analyzer/ # LLM + tree-sitter analysis +│ │ ├── graph/ # Knowledge graph builder & schema +│ │ ├── plugins/ # Plugin system for language analyzers +│ │ └── persistence/ # JSON read/write, staleness detection +│ ├── skill/ # Claude Code skill (5 commands) +│ └── dashboard/ # React + TypeScript multi-panel workspace +├── plugins/ # Built-in language analyzer plugins +│ └── tree-sitter/ # Tree-sitter based multi-language analyzer +├── docs/ +│ └── plans/ +├── package.json # Monorepo root (pnpm workspaces) +├── tsconfig.json +└── .gitignore +``` + +**Key decisions:** +- **Monorepo** (pnpm workspaces) — skill and dashboard share the core analysis engine +- **JSON interchange** — knowledge graph is a JSON file, readable by both skill and dashboard +- **Committable + auto-sync** — graph persists in `.understand-anything/`, can be committed to git, auto-detects staleness via git diff + +--- + +## Knowledge Graph Schema + +```typescript +interface KnowledgeGraph { + version: string; + project: ProjectMeta; + nodes: GraphNode[]; + edges: GraphEdge[]; + layers: Layer[]; + tour: TourStep[]; +} + +interface ProjectMeta { + name: string; + languages: string[]; + frameworks: string[]; + description: string; // LLM-generated project summary + analyzedAt: string; // ISO timestamp + gitCommitHash: string; // For staleness detection +} + +interface GraphNode { + id: string; + type: "file" | "function" | "class" | "module" | "concept"; + name: string; + filePath?: string; + lineRange?: [number, number]; + summary: string; // Plain-English description + tags: string[]; // Searchable tags + complexity: "simple" | "moderate" | "complex"; + languageNotes?: string; // Language-specific explanations +} + +interface GraphEdge { + source: string; + target: string; + type: EdgeType; + direction: "forward" | "backward" | "bidirectional"; + description?: string; + weight: number; // 0-1 importance +} + +type EdgeType = + // Structural + | "imports" | "exports" | "contains" | "inherits" | "implements" + // Behavioral + | "calls" | "subscribes" | "publishes" | "middleware" + // Data flow + | "reads_from" | "writes_to" | "transforms" | "validates" + // Dependencies + | "depends_on" | "tested_by" | "configures" + // Semantic + | "related" | "similar_to"; + +interface Layer { + id: string; + name: string; // e.g., "API Layer", "Data Layer" + description: string; + nodeIds: string[]; +} + +interface TourStep { + order: number; + title: string; + description: string; // Markdown explanation + nodeIds: string[]; // Nodes to highlight + languageLesson?: string; // Optional language concept explanation +} +``` + +--- + +## Dashboard: Multi-Panel Workspace (React + TypeScript) + +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔍 Natural Language Search: "communication layer" │ +├──────────────────────┬──────────────────────────────────┤ +│ │ │ +│ GRAPH VIEW │ CODE VIEWER │ +│ (React Flow) │ (Monaco Editor, read-only) │ +│ │ │ +│ Interactive node │ Source code + syntax highlight │ +│ graph. Click to │ LLM annotations inline. │ +│ select. Search │ │ +│ highlights. │ │ +├──────────────────────┼──────────────────────────────────┤ +│ │ │ +│ CHAT PANEL │ LEARN PANEL │ +│ │ │ +│ Context-aware Q&A │ Tour mode + Contextual mode │ +│ about selected │ Language lessons in context │ +│ nodes / project. │ of YOUR code. │ +│ │ │ +└──────────────────────┴──────────────────────────────────┘ +``` + +**Tech stack:** +- React 18 + TypeScript + Vite +- React Flow — graph visualization (built for node graphs, better than raw D3 for this) +- Monaco Editor — code viewer with syntax highlighting (same as VS Code) +- TailwindCSS — styling +- Zustand — state management (lightweight, no boilerplate) + +**Persona modes:** +- Non-technical: High-level concept nodes, code viewer hidden, learn panel expanded +- Junior dev: All panels, learn panel prominent, complexity indicators +- Experienced dev: Code viewer prominent, chat panel for deep dives + +**Natural language search:** +- Searches against node `tags`, `summary`, and `name` fields +- Uses embedding similarity if available, falls back to keyword matching +- Highlights matching nodes in the graph, filters the list + +--- + +## Claude Code Skill Commands + +| Command | Description | +|---------|-------------| +| `/understand` | Full analysis (or incremental update if graph exists) + open dashboard | +| `/understand-chat ""` | In-terminal Q&A using the knowledge graph | +| `/understand-diff` | Analyze current PR/diff — explain changes, affected areas, risks | +| `/understand-explain ` | Deep-dive explanation of a specific file or function | +| `/understand-onboard` | Generate structured onboarding guide for new team members | + +**LLM strategy:** +- Inside Claude Code → uses the active Claude session (zero extra cost) +- Standalone dashboard → users provide Claude API key for chat features +- Graph browsing, search, and learn mode work offline (pre-generated data) + +--- + +## Persistence & Staleness Detection + +``` +.understand-anything/ +├── knowledge-graph.json # The full graph (committable) +├── meta.json # Analysis metadata +│ { +│ "lastAnalyzedAt": "2026-03-14T...", +│ "gitCommitHash": "abc123", +│ "version": "1.0.0", +│ "analyzedFiles": 47 +│ } +├── cache/ # Per-file analysis cache +│ ├── src__index.ts.json +│ └── src__auth__login.ts.json +└── tours/ + └── default-tour.json +``` + +**Auto-sync flow:** +1. Skill starts → reads `meta.json` → gets last analyzed commit hash +2. Runs `git diff ..HEAD --name-only` → gets changed files +3. If no changes → serves existing graph +4. If changes → re-analyzes only changed files → merges into existing graph → updates meta + +--- + +## Plugin System + +```typescript +interface AnalyzerPlugin { + name: string; + languages: string[]; + analyzeFile(filePath: string, content: string): StructuralAnalysis; + resolveImports(filePath: string, content: string): ImportResolution[]; + extractCallGraph?(filePath: string, content: string): CallGraphEntry[]; +} +``` + +**Day 1: tree-sitter plugin** — uses `node-tree-sitter` with language grammars for: +- TypeScript/JavaScript, Python, Go, Java, Rust, C/C++ +- Extracts: function/class boundaries, import/export statements, call sites +- Combined with LLM analysis for semantic understanding + +**Future: community plugins** for language-specific deep analysis. + +--- + +## Implementation Phases + +### Phase 1: Foundation (MVP) +1. Project scaffolding — monorepo, TypeScript config, build setup +2. Core: Knowledge graph schema + JSON persistence +3. Core: LLM analysis engine (file-by-file analysis using prompts) +4. Core: tree-sitter integration for structural analysis +5. Skill: `/understand` command — analyze + persist graph +6. Dashboard: Basic React app that reads and renders the graph +7. Dashboard: Graph view with React Flow +8. Dashboard: Code viewer with Monaco Editor + +### Phase 2: Intelligence +9. Natural language search across graph nodes +10. Skill: `/understand-chat` — terminal Q&A +11. Dashboard: Chat panel with context-aware Q&A +12. Staleness detection + incremental updates +13. Layer auto-detection (group nodes into logical layers) + +### Phase 3: Learn Mode +14. Tour generation — guided project walkthrough +15. Contextual explanations — click-to-explain +16. Language-specific lessons in context of the user's code +17. Persona modes (non-technical / junior / experienced) + +### Phase 4: Advanced +18. Skill: `/understand-diff` — PR/diff analysis +19. Skill: `/understand-explain` — deep-dive on specific files +20. Skill: `/understand-onboard` — onboarding guide generation +21. Community plugin system +22. Embedding-based semantic search (optional enhancement) + +--- + +## Verification + +### How to test end-to-end: +1. **Skill analysis**: Run `/understand` on a sample project → verify `.understand-anything/knowledge-graph.json` is generated with correct schema +2. **Incremental update**: Modify a file → run `/understand` again → verify only the changed file is re-analyzed +3. **Dashboard**: Open `http://localhost:5173` → verify graph renders, nodes are clickable, search works +4. **Chat**: Ask a question in the chat panel → verify it returns a relevant answer using the knowledge graph +5. **Learn mode**: Start the tour → verify it walks through the project step by step +6. **Tree-sitter**: Analyze a TypeScript file → verify function boundaries and import relationships match the actual code + +### Test projects to validate against: +- A small TypeScript project (the tool itself) +- A Python Flask/Django API +- A Go microservice +- A mixed-language monorepo