import * as fs from "node:fs"; import * as path from "node:path"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; interface EcosystemSummary { frameworks: string[]; apis: string[]; patterns: string[]; tooling: string[]; technologyDecisions: Array<{ id: string; decision: string; confidence: number }>; } const FRAMEWORK_PATTERNS: Record = { react: ["react"], vue: ["vue"], angular: ["@angular/core"], svelte: ["svelte"], express: ["express"], fastify: ["fastify"], nestjs: ["@nestjs/core"], next: ["next"], nuxt: ["nuxt"], koa: ["koa"], jest: ["jest"], vitest: ["vitest"], }; const API_PATTERNS: Record = { graphql: ["graphql", "apollo", "@apollo"], rest: ["express", "fastify", "restana"], grpc: ["grpc", "@grpc"], websocket: ["ws", "socket.io"], }; const PATTERN_PATTERNS: Record = { microservices: ["@nestjs/microservices", "amqplib", "kafkajs"], middleware: ["express", "koa", "fastify"], cqrs: ["@nestjs/cqrs"], dependency_injection: ["inversify", "tsyringe", "@nestjs/core"], test_driven: ["jest", "vitest", "mocha"], }; const TOOLING_PATTERNS: Record = { typescript: ["typescript"], eslint: ["eslint"], prettier: ["prettier"], webpack: ["webpack"], vite: ["vite"], rollup: ["rollup"], esbuild: ["esbuild"], docker: [], ci_cd: [], }; export class ProjectResearcherAgent extends BaseAgent { readonly name = "project-researcher"; readonly description = "Researches the domain ecosystem for a new project."; readonly workflow = "research"; async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Researching project domain ecosystem..."); if (context.backend) { const result = await this.executeViaBackend( context, `Research the domain ecosystem for: ${context.specification}` ); return { ...result, duration_ms: Date.now() - start }; } const summary = this.mechanicalProjectResearch(context.project_path); const output = this.formatSummary(summary); return { success: true, output, artifacts_created: [], decisions: summary.technologyDecisions.length, escalations: 0, duration_ms: Date.now() - start, }; } mechanicalProjectResearch(projectPath: string): EcosystemSummary { const pkg = this.readPackageJson(projectPath); const tsconfig = this.readTsconfig(projectPath); const techDecisions = this.readTechDecisions(projectPath); const summary = this.categorizeFindings(pkg, tsconfig, techDecisions); return summary; } readPackageJson(projectPath: string): Record { const pkgPath = path.join(projectPath, "package.json"); if (!fs.existsSync(pkgPath)) return {}; try { return JSON.parse(fs.readFileSync(pkgPath, "utf-8")); } catch { return {}; } } readTsconfig(projectPath: string): Record { const tsconfigPath = path.join(projectPath, "tsconfig.json"); if (!fs.existsSync(tsconfigPath)) return {}; try { return JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")); } catch { return {}; } } readTechDecisions(projectPath: string): Array<{ id: string; decision: string; confidence: number }> { const decisions: Array<{ id: string; decision: string; confidence: number }> = []; const techCategories = ["technology_choice", "implementation_approach", "architecture"]; try { const { execSync } = require("node:child_process"); const logContent = execSync( `git log --all --format="%B" -100`, { cwd: projectPath, encoding: "utf-8", timeout: 5000 } ); const categoryRegex = /category:\s*(\S+)/g; const decisionRegex = /decisions:\s*\n((?:\s+-\s+.+\n?)+)/g; let catMatch; let match; const blocks: Array<{ categories: string[]; items: string[] }> = []; let currentCategories: string[] = []; while ((catMatch = categoryRegex.exec(logContent)) !== null) { currentCategories.push(catMatch[1].toLowerCase()); } while ((match = decisionRegex.exec(logContent)) !== null) { const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-")); blocks.push({ categories: [...currentCategories], items: items.map((i: string) => i.replace(/^\s*-\s*/, "").trim()), }); } for (const block of blocks) { const isTech = block.categories.some((c) => techCategories.includes(c)); if (!isTech && block.categories.length > 0) continue; for (const item of block.items) { const idMatch = item.match(/D-(\d+)/); const id = idMatch ? `D-${idMatch[1]}` : `D-${decisions.length + 1}`; const confMatch = item.match(/confidence[:\s]+(\d+\.?\d*)/); const confidence = confMatch ? parseFloat(confMatch[1]) : 0.5; decisions.push({ id, decision: item, confidence }); } } } catch { // git not available or no commits } return decisions; } categorizeFindings( pkg: Record, tsconfig: Record, techDecisions: Array<{ id: string; decision: string; confidence: number }> ): EcosystemSummary { const allDeps: string[] = []; const deps = pkg.dependencies as Record | undefined; const devDeps = pkg.devDependencies as Record | undefined; if (deps) allDeps.push(...Object.keys(deps)); if (devDeps) allDeps.push(...Object.keys(devDeps)); const frameworks: string[] = []; for (const [name, depPatterns] of Object.entries(FRAMEWORK_PATTERNS)) { if (depPatterns.some((p) => allDeps.includes(p))) { frameworks.push(name); } } const apis: string[] = []; for (const [name, depPatterns] of Object.entries(API_PATTERNS)) { if (depPatterns.some((p) => allDeps.includes(p))) { apis.push(name); } } const patterns: string[] = []; for (const [name, depPatterns] of Object.entries(PATTERN_PATTERNS)) { if (depPatterns.some((p) => allDeps.includes(p))) { patterns.push(name); } } const tooling: string[] = []; for (const [name, depPatterns] of Object.entries(TOOLING_PATTERNS)) { if (depPatterns.some((p) => allDeps.includes(p))) { tooling.push(name); } } const compilerOptions = tsconfig.compilerOptions as Record | undefined; if (compilerOptions) { const target = compilerOptions.target as string | undefined; if (target) tooling.push(`es_target:${target.toLowerCase()}`); const module = compilerOptions.module as string | undefined; if (module) tooling.push(`module_system:${module.toLowerCase()}`); } const scripts = pkg.scripts as Record | undefined; if (scripts) { if (scripts.build) tooling.push("build_script"); if (scripts.test) tooling.push("test_script"); if (scripts.lint) tooling.push("lint_script"); } const engines = pkg.engines as Record | undefined; if (engines && engines.node) { tooling.push(`node:${engines.node}`); } return { frameworks, apis, patterns, tooling, technologyDecisions: techDecisions, }; } private formatSummary(summary: EcosystemSummary): string { const lines: string[] = ["Ecosystem Summary:", ""]; lines.push("Frameworks:"); for (const f of summary.frameworks) { lines.push(` - ${f}`); } if (summary.frameworks.length === 0) lines.push(" (none detected)"); lines.push(""); lines.push("APIs:"); for (const a of summary.apis) { lines.push(` - ${a}`); } if (summary.apis.length === 0) lines.push(" (none detected)"); lines.push(""); lines.push("Patterns:"); for (const p of summary.patterns) { lines.push(` - ${p}`); } if (summary.patterns.length === 0) lines.push(" (none detected)"); lines.push(""); lines.push("Tooling:"); for (const t of summary.tooling) { lines.push(` - ${t}`); } if (summary.tooling.length === 0) lines.push(" (none detected)"); lines.push(""); lines.push("Technology Decisions:"); for (const d of summary.technologyDecisions) { lines.push(` [${d.id}|conf=${d.confidence.toFixed(2)}] ${d.decision}`); } if (summary.technologyDecisions.length === 0) lines.push(" (none found)"); return lines.join("\n"); } }