Skip to content

Commit da320b0

Browse files
fix: improve tech detection accuracy and add code examples to ARCHITECTURE.md
- Fix false positives in framework detection by removing regex-based file path matching for Express, React, Flask (they now use dependency-based detection via deps.ts) - Add detectFrameworksFromDeps() and mergeFrameworksFromDeps() to accurately detect frameworks from package.json/requirements.txt/Cargo.toml dependencies - Add support for code examples in ARCHITECTURE.md via codeExamples array - Update agent prompt to request 2-4 code examples with explanations - Add CodeExampleSchema to Zod validation - Fix edge cases in generator output (empty arrays causing broken markdown tables) - Add comprehensive tests for new framework detection functions - Update configPatterns to include more frameworks detected via config files (Astro, Remix, SvelteKit, Vite, Webpack, etc.) Tests: 205 passing
1 parent 96ec355 commit da320b0

7 files changed

Lines changed: 281 additions & 13 deletions

File tree

src/agent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Return a JSON object with this exact structure. Include "sources" arrays citing
128128
"components": [{"name": "", "description": "", "directory": ""}],
129129
"dataFlow": "",
130130
"keyAbstractions": [{"name": "", "description": ""}],
131+
"codeExamples": [{"title": "", "file": "", "code": "", "explanation": ""}],
131132
"sources": []
132133
},
133134
"firstTasks": [
@@ -155,6 +156,7 @@ Target audience: ${options.audience}
155156
156157
Provide at least 8-10 first tasks of varying difficulty. Be specific about file paths.
157158
Set runbook.applicable = false for libraries/tools that aren't deployed as services.
159+
Include 2-4 codeExamples showing key patterns/usage (short snippets of 5-15 lines with explanations).
158160
159161
REMEMBER: Limit tool calls. After reading key files, produce output immediately. Don't over-research.`;
160162
}

src/generator.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,49 @@ export function generateArchitecture(facts: RepoFacts): string {
195195
?.map((a) => `- **${a.name}**: ${a.description}`)
196196
.join("\n") || "_None documented_";
197197

198+
// Generate code examples section
199+
let codeExamplesSection = "";
200+
if (facts.architecture.codeExamples && facts.architecture.codeExamples.length > 0) {
201+
const examples = facts.architecture.codeExamples
202+
.map((ex) => {
203+
// Detect language from file extension
204+
const ext = ex.file.split('.').pop() || '';
205+
const langMap: Record<string, string> = {
206+
ts: "typescript",
207+
tsx: "typescript",
208+
js: "javascript",
209+
jsx: "javascript",
210+
py: "python",
211+
go: "go",
212+
rs: "rust",
213+
java: "java",
214+
rb: "ruby",
215+
php: "php",
216+
cs: "csharp",
217+
cpp: "cpp",
218+
c: "c",
219+
};
220+
const lang = langMap[ext] || ext;
221+
222+
return `### ${ex.title}
223+
224+
**File:** \`${ex.file}\`
225+
226+
\`\`\`${lang}
227+
${ex.code}
228+
\`\`\`
229+
230+
${ex.explanation}`;
231+
})
232+
.join("\n\n");
233+
234+
codeExamplesSection = `## Code Examples
235+
236+
${examples}
237+
238+
`;
239+
}
240+
198241
// Generate Mermaid diagram
199242
const mermaidDiagram = generateMermaidDiagram(facts);
200243

@@ -215,7 +258,7 @@ ${mermaidDiagram}
215258
216259
${components}
217260
218-
## Data Flow
261+
${codeExamplesSection}## Data Flow
219262
220263
${facts.architecture.dataFlow || "_Data flow not documented_"}
221264
@@ -227,7 +270,9 @@ ${abstractions}
227270
228271
| Path | Type | Description |
229272
|------|------|-------------|
230-
${facts.structure.entrypoints.map((e) => `| \`${e.path}\` | ${e.type} | ${e.description || "-"} |`).join("\n")}
273+
${facts.structure.entrypoints.length > 0
274+
? facts.structure.entrypoints.map((e) => `| \`${e.path}\` | ${e.type} | ${e.description || "-"} |`).join("\n")
275+
: "| _None detected_ | - | - |"}
231276
232277
## Where to Change What
233278
@@ -305,13 +350,17 @@ ${dirs}
305350
306351
| Directory | Purpose |
307352
|-----------|---------|
308-
${facts.structure.testDirs.map((d) => `| \`${d}\` | Test files |`).join("\n")}
353+
${facts.structure.testDirs.length > 0
354+
? facts.structure.testDirs.map((d) => `| \`${d}\` | Test files |`).join("\n")
355+
: "| _None detected_ | - |"}
309356
310357
## CI/CD
311358
312359
| Workflow | Triggers |
313360
|----------|----------|
314-
${facts.ci.workflows.map((w) => `| \`${w.file}\` | ${w.triggers.join(", ")} |`).join("\n")}
361+
${facts.ci.workflows.length > 0
362+
? facts.ci.workflows.map((w) => `| \`${w.file}\` | ${w.triggers.join(", ") || "-"} |`).join("\n")
363+
: "| _None detected_ | - |"}
315364
316365
## Reading Order for New Contributors
317366

src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import chalk from "chalk";
1919
import { mkdir, writeFile, rm, readFile } from "fs/promises";
2020
import { join, basename } from "path";
2121

22-
import { parseGitHubUrl, cloneRepo, scanRepo } from "./ingest.js";
22+
import { parseGitHubUrl, cloneRepo, scanRepo, mergeFrameworksFromDeps } from "./ingest.js";
2323
import { analyzeRepo, AnalysisStats } from "./agent.js";
2424
import { ProgressTracker } from "./progress.js";
2525
import { extractDependencies, generateDependencyDocs } from "./deps.js";
@@ -186,6 +186,15 @@ async function run(repoUrl: string, options: BootcampOptions): Promise<void> {
186186
// Extract dependencies
187187
const deps = await extractDependencies(repoPath);
188188

189+
// Merge frameworks detected from dependencies into stack info
190+
if (deps) {
191+
const allDepNames = [
192+
...deps.runtime.map(d => d.name),
193+
...deps.dev.map(d => d.name),
194+
];
195+
mergeFrameworksFromDeps(scanResult.stack, allDepNames);
196+
}
197+
189198
// Analyze security (read package.json for deps check)
190199
let packageJson: Record<string, unknown> | undefined;
191200
try {

src/ingest.ts

Lines changed: 116 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,25 @@ function detectStack(files: FileInfo[]): StackInfo {
172172
}
173173

174174
// Framework/build system detection from config files
175+
// NOTE: Frameworks like React, Express, Flask are NOT detected by file path patterns
176+
// as that causes false positives. They should be detected via dependency analysis (deps.ts).
177+
// Only config files that definitively indicate framework usage are listed here.
175178
const configPatterns: Record<string, { file: RegExp | string; type: "framework" | "build" | "pm" }> = {
176179
"Next.js": { file: "next.config.js", type: "framework" },
177180
"Next.js (mjs)": { file: "next.config.mjs", type: "framework" },
178-
React: { file: /react/, type: "framework" },
181+
"Next.js (ts)": { file: "next.config.ts", type: "framework" },
179182
Vue: { file: "vue.config.js", type: "framework" },
183+
"Nuxt.js": { file: "nuxt.config.js", type: "framework" },
184+
"Nuxt.js (ts)": { file: "nuxt.config.ts", type: "framework" },
180185
Angular: { file: "angular.json", type: "framework" },
181-
Express: { file: /express/, type: "framework" },
182186
Django: { file: "manage.py", type: "framework" },
183-
Flask: { file: /flask/, type: "framework" },
184187
Rails: { file: "Gemfile", type: "framework" },
188+
Astro: { file: "astro.config.mjs", type: "framework" },
189+
"Astro (ts)": { file: "astro.config.ts", type: "framework" },
190+
Remix: { file: "remix.config.js", type: "framework" },
191+
SvelteKit: { file: "svelte.config.js", type: "framework" },
192+
Vite: { file: "vite.config.ts", type: "build" },
193+
"Vite (js)": { file: "vite.config.js", type: "build" },
185194
npm: { file: "package-lock.json", type: "pm" },
186195
yarn: { file: "yarn.lock", type: "pm" },
187196
pnpm: { file: "pnpm-lock.yaml", type: "pm" },
@@ -191,6 +200,7 @@ function detectStack(files: FileInfo[]): StackInfo {
191200
cargo: { file: "Cargo.toml", type: "build" },
192201
maven: { file: "pom.xml", type: "build" },
193202
gradle: { file: "build.gradle", type: "build" },
203+
"gradle (kts)": { file: "build.gradle.kts", type: "build" },
194204
make: { file: "Makefile", type: "build" },
195205
Lake: { file: "lakefile.toml", type: "build" },
196206
"Lake (lean)": { file: "lakefile.lean", type: "build" },
@@ -200,6 +210,10 @@ function detectStack(files: FileInfo[]): StackInfo {
200210
sbt: { file: "build.sbt", type: "build" },
201211
CMake: { file: "CMakeLists.txt", type: "build" },
202212
zig: { file: "build.zig", type: "build" },
213+
Webpack: { file: "webpack.config.js", type: "build" },
214+
Rollup: { file: "rollup.config.js", type: "build" },
215+
esbuild: { file: "esbuild.config.js", type: "build" },
216+
tsup: { file: "tsup.config.ts", type: "build" },
203217
};
204218

205219
for (const [name, { file, type }] of Object.entries(configPatterns)) {
@@ -209,12 +223,14 @@ function detectStack(files: FileInfo[]): StackInfo {
209223
: filePaths.some((p) => file.test(p));
210224

211225
if (matches) {
212-
if (type === "framework" && !stack.frameworks.includes(name.replace(" (mjs)", ""))) {
213-
stack.frameworks.push(name.replace(" (mjs)", ""));
226+
// Normalize framework names by removing variant suffixes like (mjs), (ts), etc.
227+
const normalizedName = name.replace(/\s*\([^)]+\)$/, "");
228+
if (type === "framework" && !stack.frameworks.includes(normalizedName)) {
229+
stack.frameworks.push(normalizedName);
214230
} else if (type === "build" && !stack.buildSystem) {
215-
stack.buildSystem = name;
231+
stack.buildSystem = normalizedName;
216232
} else if (type === "pm" && !stack.packageManager) {
217-
stack.packageManager = name;
233+
stack.packageManager = normalizedName;
218234
}
219235
}
220236
}
@@ -541,3 +557,96 @@ export function listFilesByPattern(files: FileInfo[], pattern: string): string[]
541557
const regex = new RegExp(`^${regexPattern}$`);
542558
return files.filter((f) => !f.isDirectory && regex.test(f.path)).map((f) => f.path);
543559
}
560+
561+
/**
562+
* Known frameworks that can be detected from dependencies
563+
* Maps dependency name to display name
564+
*/
565+
const DEPENDENCY_FRAMEWORKS: Record<string, string> = {
566+
// JavaScript/TypeScript frameworks
567+
"react": "React",
568+
"react-dom": "React",
569+
"vue": "Vue",
570+
"angular": "Angular",
571+
"@angular/core": "Angular",
572+
"svelte": "Svelte",
573+
"solid-js": "Solid",
574+
"preact": "Preact",
575+
"express": "Express",
576+
"fastify": "Fastify",
577+
"hono": "Hono",
578+
"koa": "Koa",
579+
"hapi": "Hapi",
580+
"@hapi/hapi": "Hapi",
581+
"nest": "NestJS",
582+
"@nestjs/core": "NestJS",
583+
"next": "Next.js",
584+
"gatsby": "Gatsby",
585+
"nuxt": "Nuxt.js",
586+
"remix": "Remix",
587+
"@remix-run/node": "Remix",
588+
"astro": "Astro",
589+
"electron": "Electron",
590+
// Python frameworks
591+
"flask": "Flask",
592+
"django": "Django",
593+
"fastapi": "FastAPI",
594+
"tornado": "Tornado",
595+
"pyramid": "Pyramid",
596+
"bottle": "Bottle",
597+
// Go frameworks (from go.mod require)
598+
"github.com/gin-gonic/gin": "Gin",
599+
"github.com/gorilla/mux": "Gorilla Mux",
600+
"github.com/labstack/echo": "Echo",
601+
"github.com/gofiber/fiber": "Fiber",
602+
// Ruby frameworks
603+
"rails": "Rails",
604+
"sinatra": "Sinatra",
605+
// Rust frameworks
606+
"actix-web": "Actix Web",
607+
"rocket": "Rocket",
608+
"axum": "Axum",
609+
};
610+
611+
/**
612+
* Detect frameworks from dependency list
613+
* This is more accurate than file path matching
614+
*/
615+
export function detectFrameworksFromDeps(depNames: string[]): string[] {
616+
const frameworks = new Set<string>();
617+
618+
for (const dep of depNames) {
619+
const normalized = dep.toLowerCase();
620+
if (DEPENDENCY_FRAMEWORKS[normalized]) {
621+
frameworks.add(DEPENDENCY_FRAMEWORKS[normalized]);
622+
}
623+
// Also check the full dep name (for Go modules, etc.)
624+
if (DEPENDENCY_FRAMEWORKS[dep]) {
625+
frameworks.add(DEPENDENCY_FRAMEWORKS[dep]);
626+
}
627+
}
628+
629+
return Array.from(frameworks);
630+
}
631+
632+
/**
633+
* Merge frameworks detected from dependencies into stack info
634+
* Should be called after extractDependencies() in the main flow
635+
*/
636+
export function mergeFrameworksFromDeps(
637+
stack: StackInfo,
638+
depNames: string[]
639+
): StackInfo {
640+
const depFrameworks = detectFrameworksFromDeps(depNames);
641+
const existingNormalized = new Set(
642+
stack.frameworks.map(f => f.toLowerCase())
643+
);
644+
645+
for (const framework of depFrameworks) {
646+
if (!existingNormalized.has(framework.toLowerCase())) {
647+
stack.frameworks.push(framework);
648+
}
649+
}
650+
651+
return stack;
652+
}

src/schema.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,21 @@ const KeyAbstractionSchema = z.object({
9797
description: z.string(),
9898
});
9999

100+
// Code example schema
101+
const CodeExampleSchema = z.object({
102+
title: z.string(),
103+
file: z.string(),
104+
code: z.string(),
105+
explanation: z.string(),
106+
});
107+
100108
// Architecture schema
101109
const ArchitectureSchema = z.object({
102110
overview: z.string().default(""),
103111
components: z.array(ComponentSchema).default([]),
104112
dataFlow: z.string().optional(),
105113
keyAbstractions: z.array(KeyAbstractionSchema).optional(),
114+
codeExamples: z.array(CodeExampleSchema).optional(),
106115
sources: z.array(z.string()).optional(),
107116
});
108117

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ export interface RepoFacts {
189189
components: { name: string; description: string; directory: string }[];
190190
dataFlow?: string;
191191
keyAbstractions?: { name: string; description: string }[];
192+
codeExamples?: { title: string; file: string; code: string; explanation: string }[];
192193
sources?: string[];
193194
};
194195
firstTasks: FirstTask[];

0 commit comments

Comments
 (0)