Turborepo monorepo with npm workspaces: - apps/web: Next.js 14 frontend with Tailwind v4, SSE progress, doc viewer - apps/worker: BullMQ job processor (clone → parse → LLM generate) - packages/shared: TypeScript types - packages/parser: Babel-based AST parser (JS/TS) + regex (Python) - packages/llm: OpenAI/Anthropic provider abstraction + prompt pipeline - packages/diagrams: Mermaid architecture & dependency graph generators - packages/database: Prisma schema (PostgreSQL) - Docker multi-stage build (web + worker targets) All packages compile successfully with tsc and next build.
154 lines
4.9 KiB
TypeScript
154 lines
4.9 KiB
TypeScript
import type { CodeStructure, GeneratedDocs, FileNode } from "@codeboard/shared";
|
|
import type { LLMProvider } from "./providers/base.js";
|
|
import { buildArchitecturePrompt } from "./prompts/architecture-overview.js";
|
|
import { buildModuleSummaryPrompt } from "./prompts/module-summary.js";
|
|
import { buildPatternsPrompt } from "./prompts/patterns-detection.js";
|
|
import { buildGettingStartedPrompt } from "./prompts/getting-started.js";
|
|
|
|
function parseSection(text: string, header: string): string {
|
|
const regex = new RegExp(`## ${header}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
const match = regex.exec(text);
|
|
return match?.[1]?.trim() ?? "";
|
|
}
|
|
|
|
function parseMermaid(text: string): string {
|
|
const match = /```mermaid\s*\n([\s\S]*?)```/.exec(text);
|
|
return match?.[1]?.trim() ?? "flowchart TD\n A[No diagram generated]";
|
|
}
|
|
|
|
function parseList(text: string): string[] {
|
|
return text
|
|
.split("\n")
|
|
.map((l) => l.replace(/^[-*]\s*/, "").trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export async function generateDocumentation(
|
|
codeStructure: CodeStructure,
|
|
provider: LLMProvider,
|
|
onProgress?: (stage: string, progress: number) => void
|
|
): Promise<GeneratedDocs> {
|
|
onProgress?.("architecture", 10);
|
|
|
|
const archMessages = buildArchitecturePrompt(codeStructure);
|
|
const archResponse = await provider.chat(archMessages);
|
|
|
|
const architectureOverview = parseSection(archResponse, "Architecture Overview");
|
|
const techStackRaw = parseSection(archResponse, "Tech Stack");
|
|
const architectureDiagram = parseMermaid(archResponse);
|
|
const techStack = techStackRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
|
|
onProgress?.("modules", 30);
|
|
|
|
const moduleLimit = Math.min(codeStructure.modules.length, 10);
|
|
const moduleSummaries = await Promise.all(
|
|
codeStructure.modules.slice(0, moduleLimit).map(async (mod) => {
|
|
const moduleFiles: FileNode[] = codeStructure.files.filter((f) =>
|
|
mod.files.includes(f.path)
|
|
);
|
|
|
|
if (moduleFiles.length === 0) {
|
|
return {
|
|
name: mod.name,
|
|
path: mod.path,
|
|
summary: "Empty module — no parseable files found.",
|
|
keyFiles: [],
|
|
publicApi: [],
|
|
dependsOn: [],
|
|
dependedBy: [],
|
|
};
|
|
}
|
|
|
|
const messages = buildModuleSummaryPrompt(mod, moduleFiles);
|
|
const response = await provider.chat(messages, { model: undefined });
|
|
|
|
const summary = parseSection(response, "Summary");
|
|
const keyFilesRaw = parseList(parseSection(response, "Key Files"));
|
|
const publicApi = parseList(parseSection(response, "Public API"));
|
|
|
|
const dependsOn = [
|
|
...new Set(
|
|
moduleFiles.flatMap((f) =>
|
|
f.imports
|
|
.map((imp) => imp.source)
|
|
.filter((s) => !s.startsWith("."))
|
|
)
|
|
),
|
|
].slice(0, 10);
|
|
|
|
const dependedBy = codeStructure.dependencies
|
|
.filter((d) => mod.files.includes(d.target))
|
|
.map((d) => d.source)
|
|
.filter((s) => !mod.files.includes(s))
|
|
.slice(0, 10);
|
|
|
|
return {
|
|
name: mod.name,
|
|
path: mod.path,
|
|
summary: summary || "Module analyzed but no summary generated.",
|
|
keyFiles: keyFilesRaw.map((kf) => ({ path: kf, purpose: "" })),
|
|
publicApi,
|
|
dependsOn,
|
|
dependedBy,
|
|
};
|
|
})
|
|
);
|
|
|
|
onProgress?.("patterns", 60);
|
|
|
|
const patternsMessages = buildPatternsPrompt(codeStructure);
|
|
const patternsResponse = await provider.chat(patternsMessages);
|
|
|
|
const conventions = parseList(parseSection(patternsResponse, "Coding Conventions"));
|
|
const designPatterns = parseList(parseSection(patternsResponse, "Design Patterns"));
|
|
const architecturalDecisions = parseList(parseSection(patternsResponse, "Architectural Decisions"));
|
|
|
|
onProgress?.("getting-started", 80);
|
|
|
|
const gsMessages = buildGettingStartedPrompt(
|
|
codeStructure,
|
|
architectureOverview
|
|
);
|
|
const gsResponse = await provider.chat(gsMessages);
|
|
|
|
const prerequisites = parseList(parseSection(gsResponse, "Prerequisites"));
|
|
const setupSteps = parseList(parseSection(gsResponse, "Setup Steps"));
|
|
const firstTask = parseSection(gsResponse, "Your First Task");
|
|
|
|
onProgress?.("complete", 100);
|
|
|
|
const languages = [...new Set(codeStructure.files.map((f) => f.language))];
|
|
|
|
return {
|
|
id: "",
|
|
repoUrl: "",
|
|
repoName: "",
|
|
generatedAt: new Date().toISOString(),
|
|
sections: {
|
|
overview: {
|
|
title: "Architecture Overview",
|
|
description: architectureOverview,
|
|
architectureDiagram,
|
|
techStack,
|
|
keyMetrics: {
|
|
files: codeStructure.files.length,
|
|
modules: codeStructure.modules.length,
|
|
languages,
|
|
},
|
|
},
|
|
modules: moduleSummaries,
|
|
patterns: {
|
|
conventions,
|
|
designPatterns,
|
|
architecturalDecisions,
|
|
},
|
|
gettingStarted: {
|
|
prerequisites,
|
|
setupSteps,
|
|
firstTask,
|
|
},
|
|
dependencyGraph: architectureDiagram,
|
|
},
|
|
};
|
|
}
|