diff --git a/packages/core/src/analyzer/graph-builder.test.ts b/packages/core/src/analyzer/graph-builder.test.ts new file mode 100644 index 0000000..224d385 --- /dev/null +++ b/packages/core/src/analyzer/graph-builder.test.ts @@ -0,0 +1,215 @@ +import { describe, it, expect } from "vitest"; +import { GraphBuilder } from "./graph-builder.js"; +import type { StructuralAnalysis } from "../types.js"; + +describe("GraphBuilder", () => { + it("should create file nodes from file list", () => { + const builder = new GraphBuilder("test-project", "abc123"); + + builder.addFile("src/index.ts", { + summary: "Entry point", + tags: ["entry"], + complexity: "simple", + }); + builder.addFile("src/utils.ts", { + summary: "Utility functions", + tags: ["utility"], + complexity: "moderate", + }); + + const graph = builder.build(); + + expect(graph.nodes).toHaveLength(2); + expect(graph.nodes[0]).toMatchObject({ + id: "file:src/index.ts", + type: "file", + name: "index.ts", + filePath: "src/index.ts", + summary: "Entry point", + tags: ["entry"], + complexity: "simple", + }); + expect(graph.nodes[1]).toMatchObject({ + id: "file:src/utils.ts", + type: "file", + name: "utils.ts", + filePath: "src/utils.ts", + summary: "Utility functions", + }); + }); + + it("should create function and class nodes from structural analysis", () => { + const builder = new GraphBuilder("test-project", "abc123"); + const analysis: StructuralAnalysis = { + functions: [ + { name: "processData", lineRange: [10, 25], params: ["input"], returnType: "string" }, + { name: "validate", lineRange: [30, 40], params: ["data"] }, + ], + classes: [ + { name: "DataStore", lineRange: [50, 100], methods: ["get", "set"], properties: ["data"] }, + ], + imports: [], + exports: [], + }; + + builder.addFileWithAnalysis("src/service.ts", analysis, { + summary: "Service module", + tags: ["service"], + complexity: "complex", + fileSummary: "Handles data processing", + summaries: { + processData: "Processes raw input data", + validate: "Validates data format", + DataStore: "Manages stored data", + }, + }); + + const graph = builder.build(); + + // 1 file + 2 functions + 1 class = 4 nodes + expect(graph.nodes).toHaveLength(4); + + const fileNode = graph.nodes.find((n) => n.id === "file:src/service.ts"); + expect(fileNode).toBeDefined(); + expect(fileNode!.type).toBe("file"); + expect(fileNode!.summary).toBe("Handles data processing"); + + const funcNode = graph.nodes.find((n) => n.id === "func:src/service.ts:processData"); + expect(funcNode).toBeDefined(); + expect(funcNode!.type).toBe("function"); + expect(funcNode!.name).toBe("processData"); + expect(funcNode!.lineRange).toEqual([10, 25]); + expect(funcNode!.summary).toBe("Processes raw input data"); + + const validateNode = graph.nodes.find((n) => n.id === "func:src/service.ts:validate"); + expect(validateNode).toBeDefined(); + expect(validateNode!.summary).toBe("Validates data format"); + + const classNode = graph.nodes.find((n) => n.id === "class:src/service.ts:DataStore"); + expect(classNode).toBeDefined(); + expect(classNode!.type).toBe("class"); + expect(classNode!.name).toBe("DataStore"); + expect(classNode!.summary).toBe("Manages stored data"); + }); + + it("should create contains edges between files and their functions/classes", () => { + const builder = new GraphBuilder("test-project", "abc123"); + const analysis: StructuralAnalysis = { + functions: [ + { name: "helper", lineRange: [5, 15], params: [] }, + ], + classes: [ + { name: "Widget", lineRange: [20, 50], methods: [], properties: [] }, + ], + imports: [], + exports: [], + }; + + builder.addFileWithAnalysis("src/widget.ts", analysis, { + summary: "Widget module", + tags: [], + complexity: "moderate", + fileSummary: "Widget component", + summaries: { helper: "Helper function", Widget: "Widget class" }, + }); + + const graph = builder.build(); + + const containsEdges = graph.edges.filter((e) => e.type === "contains"); + expect(containsEdges).toHaveLength(2); + + expect(containsEdges[0]).toMatchObject({ + source: "file:src/widget.ts", + target: "func:src/widget.ts:helper", + type: "contains", + direction: "forward", + weight: 1, + }); + expect(containsEdges[1]).toMatchObject({ + source: "file:src/widget.ts", + target: "class:src/widget.ts:Widget", + type: "contains", + direction: "forward", + weight: 1, + }); + }); + + it("should create import edges between files", () => { + const builder = new GraphBuilder("test-project", "abc123"); + + builder.addFile("src/index.ts", { + summary: "Entry", + tags: [], + complexity: "simple", + }); + builder.addFile("src/utils.ts", { + summary: "Utils", + tags: [], + complexity: "simple", + }); + + 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); + expect(importEdges[0]).toMatchObject({ + source: "file:src/index.ts", + target: "file:src/utils.ts", + type: "imports", + direction: "forward", + }); + }); + + it("should create call edges between functions", () => { + const builder = new GraphBuilder("test-project", "abc123"); + + builder.addCallEdge("src/index.ts", "main", "src/utils.ts", "helper"); + + const graph = builder.build(); + const callEdges = graph.edges.filter((e) => e.type === "calls"); + expect(callEdges).toHaveLength(1); + expect(callEdges[0]).toMatchObject({ + source: "func:src/index.ts:main", + target: "func:src/utils.ts:helper", + type: "calls", + direction: "forward", + }); + }); + + it("should set project metadata correctly", () => { + const builder = new GraphBuilder("my-awesome-project", "deadbeef"); + + builder.addFile("src/app.ts", { + summary: "App", + tags: [], + complexity: "simple", + }); + builder.addFile("src/script.py", { + summary: "Script", + tags: [], + complexity: "simple", + }); + + const graph = builder.build(); + + expect(graph.version).toBe("1.0.0"); + expect(graph.project.name).toBe("my-awesome-project"); + expect(graph.project.gitCommitHash).toBe("deadbeef"); + expect(graph.project.languages).toEqual(["python", "typescript"]); + expect(graph.project.analyzedAt).toBeTruthy(); + expect(graph.layers).toEqual([]); + expect(graph.tour).toEqual([]); + }); + + it("should detect languages from file extensions", () => { + const builder = new GraphBuilder("polyglot", "hash123"); + + builder.addFile("main.go", { summary: "", tags: [], complexity: "simple" }); + builder.addFile("lib.rs", { summary: "", tags: [], complexity: "simple" }); + builder.addFile("app.js", { summary: "", tags: [], complexity: "simple" }); + + const graph = builder.build(); + expect(graph.project.languages).toEqual(["go", "javascript", "rust"]); + }); +}); diff --git a/packages/core/src/analyzer/graph-builder.ts b/packages/core/src/analyzer/graph-builder.ts new file mode 100644 index 0000000..9c0c97d --- /dev/null +++ b/packages/core/src/analyzer/graph-builder.ts @@ -0,0 +1,207 @@ +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; +} + +const EXTENSION_LANGUAGE: Record = { + ".ts": "typescript", + ".tsx": "typescript", + ".js": "javascript", + ".jsx": "javascript", + ".mjs": "javascript", + ".cjs": "javascript", + ".py": "python", + ".rb": "ruby", + ".go": "go", + ".rs": "rust", + ".java": "java", + ".kt": "kotlin", + ".swift": "swift", + ".c": "c", + ".cpp": "cpp", + ".h": "c", + ".hpp": "cpp", + ".cs": "csharp", + ".php": "php", + ".lua": "lua", + ".sh": "shell", + ".bash": "shell", + ".zsh": "shell", + ".json": "json", + ".yaml": "yaml", + ".yml": "yaml", + ".toml": "toml", + ".xml": "xml", + ".html": "html", + ".css": "css", + ".scss": "scss", + ".less": "less", + ".md": "markdown", + ".sql": "sql", +}; + +function detectLanguage(filePath: string): string { + const lastDot = filePath.lastIndexOf("."); + if (lastDot === -1) return "unknown"; + const ext = filePath.slice(lastDot).toLowerCase(); + return EXTENSION_LANGUAGE[ext] ?? "unknown"; +} + +export class GraphBuilder { + private nodes: GraphNode[] = []; + private edges: GraphEdge[] = []; + private languages = new Set(); + private projectName: string; + private gitHash: string; + + constructor(projectName: string, gitHash: string) { + this.projectName = projectName; + this.gitHash = gitHash; + } + + addFile(filePath: string, meta: FileMeta): void { + const lang = detectLanguage(filePath); + if (lang !== "unknown") { + this.languages.add(lang); + } + + const name = filePath.split("/").pop() ?? filePath; + + this.nodes.push({ + id: `file:${filePath}`, + type: "file", + name, + filePath, + summary: meta.summary, + tags: meta.tags, + complexity: meta.complexity, + }); + } + + addFileWithAnalysis( + filePath: string, + analysis: StructuralAnalysis, + meta: FileAnalysisMeta, + ): void { + const lang = detectLanguage(filePath); + if (lang !== "unknown") { + this.languages.add(lang); + } + + const fileName = filePath.split("/").pop() ?? filePath; + const fileId = `file:${filePath}`; + + // Create the file node + this.nodes.push({ + id: fileId, + type: "file", + name: fileName, + filePath, + summary: meta.fileSummary, + tags: meta.tags, + complexity: meta.complexity, + }); + + // Create function nodes with "contains" edges + for (const fn of analysis.functions) { + const funcId = `func:${filePath}:${fn.name}`; + this.nodes.push({ + id: funcId, + type: "function", + name: fn.name, + filePath, + lineRange: fn.lineRange, + summary: meta.summaries[fn.name] ?? "", + tags: [], + complexity: meta.complexity, + }); + + this.edges.push({ + source: fileId, + target: funcId, + type: "contains", + direction: "forward", + weight: 1, + }); + } + + // Create class nodes with "contains" edges + for (const cls of analysis.classes) { + const classId = `class:${filePath}:${cls.name}`; + this.nodes.push({ + id: classId, + type: "class", + name: cls.name, + filePath, + lineRange: cls.lineRange, + summary: meta.summaries[cls.name] ?? "", + tags: [], + complexity: meta.complexity, + }); + + this.edges.push({ + source: fileId, + target: classId, + type: "contains", + direction: "forward", + weight: 1, + }); + } + } + + addImportEdge(fromFile: string, toFile: string): void { + this.edges.push({ + source: `file:${fromFile}`, + target: `file:${toFile}`, + type: "imports", + direction: "forward", + weight: 0.7, + }); + } + + addCallEdge( + callerFile: string, + callerFunc: string, + calleeFile: string, + calleeFunc: string, + ): void { + this.edges.push({ + source: `func:${callerFile}:${callerFunc}`, + target: `func:${calleeFile}:${calleeFunc}`, + type: "calls", + direction: "forward", + weight: 0.8, + }); + } + + build(): KnowledgeGraph { + return { + version: "1.0.0", + project: { + name: this.projectName, + languages: [...this.languages].sort(), + frameworks: [], + description: "", + analyzedAt: new Date().toISOString(), + gitCommitHash: this.gitHash, + }, + nodes: this.nodes, + edges: this.edges, + layers: [], + tour: [], + }; + } +} diff --git a/packages/core/src/analyzer/llm-analyzer.test.ts b/packages/core/src/analyzer/llm-analyzer.test.ts new file mode 100644 index 0000000..843bd04 --- /dev/null +++ b/packages/core/src/analyzer/llm-analyzer.test.ts @@ -0,0 +1,248 @@ +import { describe, it, expect } from "vitest"; +import { + buildFileAnalysisPrompt, + buildProjectSummaryPrompt, + parseFileAnalysisResponse, + parseProjectSummaryResponse, +} from "./llm-analyzer.js"; + +describe("LLM Analyzer", () => { + describe("buildFileAnalysisPrompt", () => { + it("should include file path and content in the prompt", () => { + const prompt = buildFileAnalysisPrompt( + "src/utils.ts", + "export function add(a: number, b: number) { return a + b; }", + "A math utility library", + ); + + expect(prompt).toContain("src/utils.ts"); + expect(prompt).toContain("export function add"); + expect(prompt).toContain("A math utility library"); + expect(prompt).toContain("fileSummary"); + expect(prompt).toContain("JSON"); + }); + + it("should include project context", () => { + const prompt = buildFileAnalysisPrompt( + "app.py", + "print('hello')", + "A Python web server", + ); + + expect(prompt).toContain("A Python web server"); + }); + }); + + describe("parseFileAnalysisResponse", () => { + it("should parse valid JSON response", () => { + const response = JSON.stringify({ + fileSummary: "A utility module for string processing", + tags: ["utility", "string"], + complexity: "simple", + functionSummaries: { capitalize: "Capitalizes the first letter" }, + classSummaries: {}, + languageNotes: "Uses ES2022 features", + }); + + const result = parseFileAnalysisResponse(response); + + expect(result).not.toBeNull(); + expect(result!.fileSummary).toBe("A utility module for string processing"); + expect(result!.tags).toEqual(["utility", "string"]); + expect(result!.complexity).toBe("simple"); + expect(result!.functionSummaries).toEqual({ + capitalize: "Capitalizes the first letter", + }); + expect(result!.classSummaries).toEqual({}); + expect(result!.languageNotes).toBe("Uses ES2022 features"); + }); + + it("should handle markdown-wrapped JSON (```json ... ```)", () => { + const response = `Here is the analysis: + +\`\`\`json +{ + "fileSummary": "Database connection handler", + "tags": ["database", "connection"], + "complexity": "complex", + "functionSummaries": { "connect": "Establishes DB connection" }, + "classSummaries": { "Pool": "Connection pool manager" } +} +\`\`\` + +That's the analysis.`; + + const result = parseFileAnalysisResponse(response); + + expect(result).not.toBeNull(); + expect(result!.fileSummary).toBe("Database connection handler"); + expect(result!.tags).toEqual(["database", "connection"]); + expect(result!.complexity).toBe("complex"); + expect(result!.functionSummaries.connect).toBe("Establishes DB connection"); + expect(result!.classSummaries.Pool).toBe("Connection pool manager"); + }); + + it("should handle markdown fences without language tag", () => { + const response = `\`\`\` +{ + "fileSummary": "Config loader", + "tags": ["config"], + "complexity": "simple", + "functionSummaries": {}, + "classSummaries": {} +} +\`\`\``; + + const result = parseFileAnalysisResponse(response); + + expect(result).not.toBeNull(); + expect(result!.fileSummary).toBe("Config loader"); + }); + + it("should return null for invalid JSON", () => { + const result = parseFileAnalysisResponse("This is not JSON at all"); + expect(result).toBeNull(); + }); + + it("should return null for completely empty response", () => { + const result = parseFileAnalysisResponse(""); + expect(result).toBeNull(); + }); + + it("should default complexity to 'moderate' for unknown values", () => { + const response = JSON.stringify({ + fileSummary: "Some file", + tags: [], + complexity: "very-hard", + functionSummaries: {}, + classSummaries: {}, + }); + + const result = parseFileAnalysisResponse(response); + + expect(result).not.toBeNull(); + expect(result!.complexity).toBe("moderate"); + }); + + it("should default complexity to 'moderate' when missing", () => { + const response = JSON.stringify({ + fileSummary: "Some file", + tags: [], + functionSummaries: {}, + classSummaries: {}, + }); + + const result = parseFileAnalysisResponse(response); + + expect(result).not.toBeNull(); + expect(result!.complexity).toBe("moderate"); + }); + + it("should handle missing optional fields gracefully", () => { + const response = JSON.stringify({ + fileSummary: "Minimal response", + }); + + const result = parseFileAnalysisResponse(response); + + expect(result).not.toBeNull(); + expect(result!.fileSummary).toBe("Minimal response"); + expect(result!.tags).toEqual([]); + expect(result!.complexity).toBe("moderate"); + expect(result!.functionSummaries).toEqual({}); + expect(result!.classSummaries).toEqual({}); + expect(result!.languageNotes).toBeUndefined(); + }); + }); + + describe("buildProjectSummaryPrompt", () => { + it("should include file list in the prompt", () => { + const fileList = ["src/index.ts", "src/utils.ts", "package.json"]; + const prompt = buildProjectSummaryPrompt(fileList, []); + + expect(prompt).toContain("src/index.ts"); + expect(prompt).toContain("src/utils.ts"); + expect(prompt).toContain("package.json"); + expect(prompt).toContain("description"); + expect(prompt).toContain("frameworks"); + expect(prompt).toContain("layers"); + }); + + it("should include sample file contents when provided", () => { + const prompt = buildProjectSummaryPrompt( + ["src/app.ts"], + [{ path: "src/app.ts", content: "const app = express();" }], + ); + + expect(prompt).toContain("src/app.ts"); + expect(prompt).toContain("const app = express()"); + }); + }); + + describe("parseProjectSummaryResponse", () => { + it("should parse valid project summary response", () => { + const response = JSON.stringify({ + description: "A REST API for managing tasks", + frameworks: ["Express", "TypeScript", "Vitest"], + layers: [ + { + name: "API", + description: "HTTP route handlers", + filePatterns: ["src/routes/**"], + }, + { + name: "Data", + description: "Database access layer", + filePatterns: ["src/db/**", "src/models/**"], + }, + ], + }); + + const result = parseProjectSummaryResponse(response); + + expect(result).not.toBeNull(); + expect(result!.description).toBe("A REST API for managing tasks"); + expect(result!.frameworks).toEqual(["Express", "TypeScript", "Vitest"]); + expect(result!.layers).toHaveLength(2); + expect(result!.layers[0]).toEqual({ + name: "API", + description: "HTTP route handlers", + filePatterns: ["src/routes/**"], + }); + }); + + it("should handle markdown-wrapped response", () => { + const response = `\`\`\`json +{ + "description": "A CLI tool", + "frameworks": ["Commander"], + "layers": [] +} +\`\`\``; + + const result = parseProjectSummaryResponse(response); + + expect(result).not.toBeNull(); + expect(result!.description).toBe("A CLI tool"); + expect(result!.frameworks).toEqual(["Commander"]); + }); + + it("should return null for invalid JSON", () => { + const result = parseProjectSummaryResponse("Not valid JSON"); + expect(result).toBeNull(); + }); + + it("should handle missing fields gracefully", () => { + const response = JSON.stringify({ + description: "Some project", + }); + + const result = parseProjectSummaryResponse(response); + + expect(result).not.toBeNull(); + expect(result!.description).toBe("Some project"); + expect(result!.frameworks).toEqual([]); + expect(result!.layers).toEqual([]); + }); + }); +}); diff --git a/packages/core/src/analyzer/llm-analyzer.ts b/packages/core/src/analyzer/llm-analyzer.ts new file mode 100644 index 0000000..5e9b503 --- /dev/null +++ b/packages/core/src/analyzer/llm-analyzer.ts @@ -0,0 +1,186 @@ +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 a prompt for analyzing a single source file with an LLM. + */ +export function buildFileAnalysisPrompt( + filePath: string, + content: string, + projectContext: string, +): string { + return `You are a code analysis assistant. Analyze the following source file and return a JSON object. + +Project context: ${projectContext} + +File: ${filePath} + +\`\`\` +${content} +\`\`\` + +Return a JSON object with the following fields: +- "fileSummary": A concise summary of what this file does (1-2 sentences). +- "tags": An array of relevant tags (e.g., ["utility", "async", "api"]). +- "complexity": One of "simple", "moderate", or "complex". +- "functionSummaries": An object mapping function names to 1-sentence summaries. +- "classSummaries": An object mapping class names to 1-sentence summaries. +- "languageNotes": Optional notes about language-specific patterns or idioms used. + +Respond ONLY with the JSON object, no additional text.`; +} + +/** + * Generates a prompt for creating a project-level summary with an LLM. + */ +export function buildProjectSummaryPrompt( + fileList: string[], + sampleFiles: Array<{ path: string; content: string }>, +): string { + const fileListStr = fileList.map((f) => ` - ${f}`).join("\n"); + + let samplesStr = ""; + if (sampleFiles.length > 0) { + samplesStr = "\n\nSample files:\n"; + for (const sample of sampleFiles) { + samplesStr += `\n--- ${sample.path} ---\n\`\`\`\n${sample.content}\n\`\`\`\n`; + } + } + + return `You are a code analysis assistant. Analyze the following project structure and return a JSON object describing the project. + +File list: +${fileListStr}${samplesStr} + +Return a JSON object with the following fields: +- "description": A concise description of what this project does (2-3 sentences). +- "frameworks": An array of frameworks and major libraries detected (e.g., ["React", "Express", "Vitest"]). +- "layers": An array of logical layers, each with: + - "name": The layer name (e.g., "API", "Data", "UI"). + - "description": What this layer is responsible for. + - "filePatterns": Glob patterns or path prefixes that belong to this layer. + +Respond ONLY with the JSON object, no additional text.`; +} + +/** + * Extracts a JSON block from an LLM response, handling markdown fences. + */ +function extractJson(response: string): string { + // Try to extract from markdown code fences + const fenceMatch = response.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/); + if (fenceMatch) { + return fenceMatch[1].trim(); + } + + // Try to find a raw JSON object + const objectMatch = response.match(/\{[\s\S]*\}/); + if (objectMatch) { + return objectMatch[0].trim(); + } + + return response.trim(); +} + +const VALID_COMPLEXITIES = new Set(["simple", "moderate", "complex"]); + +/** + * Parses an LLM response for file analysis. Returns null if parsing fails. + */ +export function parseFileAnalysisResponse( + response: string, +): LLMFileAnalysis | null { + try { + const jsonStr = extractJson(response); + const parsed = JSON.parse(jsonStr); + + // Validate and normalize complexity + let complexity: "simple" | "moderate" | "complex" = "moderate"; + if ( + typeof parsed.complexity === "string" && + VALID_COMPLEXITIES.has(parsed.complexity) + ) { + complexity = parsed.complexity as "simple" | "moderate" | "complex"; + } + + return { + fileSummary: + typeof parsed.fileSummary === "string" ? parsed.fileSummary : "", + tags: Array.isArray(parsed.tags) + ? parsed.tags.filter((t: unknown) => typeof t === "string") + : [], + complexity, + functionSummaries: + typeof parsed.functionSummaries === "object" && + parsed.functionSummaries !== null + ? parsed.functionSummaries + : {}, + classSummaries: + typeof parsed.classSummaries === "object" && + parsed.classSummaries !== null + ? parsed.classSummaries + : {}, + languageNotes: + typeof parsed.languageNotes === "string" + ? parsed.languageNotes + : undefined, + }; + } catch { + return null; + } +} + +/** + * Parses an LLM response for project summary. Returns null if parsing fails. + */ +export function parseProjectSummaryResponse( + response: string, +): LLMProjectSummary | null { + try { + const jsonStr = extractJson(response); + const parsed = JSON.parse(jsonStr); + + return { + description: + typeof parsed.description === "string" ? parsed.description : "", + frameworks: Array.isArray(parsed.frameworks) + ? parsed.frameworks.filter((f: unknown) => typeof f === "string") + : [], + layers: Array.isArray(parsed.layers) + ? parsed.layers + .filter( + (l: unknown): l is { name: string; description: string; filePatterns: string[] } => + typeof l === "object" && + l !== null && + typeof (l as Record).name === "string", + ) + .map( + (l: { name: string; description: string; filePatterns: string[] }) => ({ + name: l.name, + description: + typeof l.description === "string" ? l.description : "", + filePatterns: Array.isArray(l.filePatterns) + ? l.filePatterns.filter( + (p: unknown) => typeof p === "string", + ) + : [], + }), + ) + : [], + }; + } catch { + return null; + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3ab702e..727dc43 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,11 @@ 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";