Files
Fulfilled-Knowledge/Understand-Anything-main/docs/superpowers/plans/2026-03-14-phase1-implementation.md
2026-05-27 15:40:32 +08:00

2470 lines
66 KiB
Markdown

# 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<string, string>; // 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<string> = 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<string, string> = {
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<string, string>;
classSummaries: Record<string, string>;
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<DashboardStore>()((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<string, string> = {
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<string, string> = {
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<CustomNodeData>) {
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 (
<div
className={`rounded-lg border px-3 py-2 min-w-[140px] max-w-[220px] ${colorClass} ${highlightClass} ${selectedClass}`}
>
<Handle type="target" position={Position.Top} className="!bg-gray-400" />
<div className="flex items-center gap-1.5 mb-1">
<span className="text-[10px] uppercase tracking-wider text-gray-400">
{data.nodeType}
</span>
<span
className={`text-[9px] px-1 rounded ${complexityBadge[data.complexity]}`}
>
{data.complexity}
</span>
</div>
<div className="text-sm font-medium text-white truncate">
{data.label}
</div>
<div className="text-xs text-gray-400 mt-0.5 line-clamp-2">
{data.summary}
</div>
<Handle
type="source"
position={Position.Bottom}
className="!bg-gray-400"
/>
</div>
);
}
```
**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 (
<div className="flex items-center justify-center h-full text-gray-500">
No knowledge graph loaded. Run /understand first.
</div>
);
}
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
nodeTypes={nodeTypes}
colorMode="dark"
fitView
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
);
}
```
**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<string, string> = {
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 (
<div className="flex items-center justify-center h-full text-gray-500">
Select a node to view its source code.
</div>
);
}
// 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 (
<div className="h-full flex flex-col">
<div className="px-3 py-1.5 bg-gray-800 border-b border-gray-700 text-sm text-gray-300 flex items-center gap-2">
<span className="text-gray-500">
{selectedNode.type.toUpperCase()}
</span>
<span className="font-medium">{selectedNode.name}</span>
<span className="text-gray-500 ml-auto">
{selectedNode.filePath}
</span>
</div>
<div className="flex-1">
<Editor
height="100%"
language={detectLanguage(selectedNode.filePath)}
value={placeholder}
theme="vs-dark"
options={{
readOnly: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 13,
lineNumbers: "on",
wordWrap: "on",
}}
/>
</div>
</div>
);
}
```
**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 (
<div className="flex items-center gap-3 px-4 py-2 bg-gray-800 border-b border-gray-700">
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<input
type="text"
value={searchQuery}
onChange={(e) => 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 && (
<span className="text-xs text-gray-400">
{searchResults.length} results
</span>
)}
</div>
);
}
```
**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 (
<div className="h-full flex items-center justify-center text-gray-500 p-4 text-sm">
Select a node to see details. Chat and Learn panels coming in Phase 2.
</div>
);
}
// Find connected nodes
const connectedEdges = graph?.edges.filter(
(e) => e.source === selectedNodeId || e.target === selectedNodeId,
) || [];
return (
<div className="h-full overflow-y-auto p-4 text-sm">
<h3 className="text-lg font-semibold text-white mb-1">
{selectedNode.name}
</h3>
<div className="flex gap-2 mb-3">
<span className="px-2 py-0.5 bg-gray-700 rounded text-xs text-gray-300">
{selectedNode.type}
</span>
<span className="px-2 py-0.5 bg-gray-700 rounded text-xs text-gray-300">
{selectedNode.complexity}
</span>
</div>
<p className="text-gray-300 mb-3">{selectedNode.summary}</p>
{selectedNode.languageNotes && (
<div className="mb-3 p-2 bg-blue-900/30 border border-blue-800 rounded">
<div className="text-xs text-blue-400 mb-1">Language Note</div>
<p className="text-gray-300 text-xs">{selectedNode.languageNotes}</p>
</div>
)}
{selectedNode.tags.length > 0 && (
<div className="mb-3">
<div className="text-xs text-gray-500 mb-1">Tags</div>
<div className="flex flex-wrap gap-1">
{selectedNode.tags.map((tag) => (
<span
key={tag}
className="px-1.5 py-0.5 bg-gray-700 rounded text-xs text-gray-300"
>
{tag}
</span>
))}
</div>
</div>
)}
{connectedEdges.length > 0 && (
<div>
<div className="text-xs text-gray-500 mb-1">
Connections ({connectedEdges.length})
</div>
<div className="space-y-1">
{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 (
<div key={i} className="text-xs text-gray-400">
{direction} {edge.type}: {otherNode?.name || otherId}
</div>
);
})}
</div>
</div>
)}
</div>
);
}
```
**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<KnowledgeGraph | null> {
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 (
<div className="h-screen w-screen flex flex-col bg-gray-900 text-white">
{/* Header */}
<div className="flex items-center gap-3 px-4 py-2 bg-gray-950 border-b border-gray-800">
<h1 className="text-sm font-bold tracking-wider">
UNDERSTAND ANYTHING
</h1>
{graph && (
<span className="text-xs text-gray-500">
{graph.project.name} &middot; {graph.nodes.length} nodes &middot;{" "}
{graph.edges.length} edges
</span>
)}
</div>
{/* Search */}
<SearchBar />
{/* Main workspace: 2x2 grid */}
<div className="flex-1 grid grid-cols-2 grid-rows-2 gap-px bg-gray-700 overflow-hidden">
{/* Top-left: Graph View */}
<div className="bg-gray-900">
<GraphView />
</div>
{/* Top-right: Code Viewer */}
<div className="bg-gray-900">
<CodeViewer />
</div>
{/* Bottom-left: Chat Panel (placeholder) */}
<div className="bg-gray-900 flex items-center justify-center text-gray-600 text-sm">
Chat panel coming in Phase 2
</div>
{/* Bottom-right: Node Info / Learn Panel */}
<div className="bg-gray-900">
<NodeInfo />
</div>
</div>
</div>
);
}
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