From ae0165a802c620dc8dfb7ee15e76fd22a4c40155 Mon Sep 17 00:00:00 2001 From: Debian Date: Sat, 10 Jan 2026 12:17:42 +0000 Subject: [PATCH] Implement Phase 2: Core API Integration - Add Claude API client with Anthropic SDK - Retry logic with exponential backoff (3 attempts) - Handle auth errors, rate limits, and server errors - Architecture classification with JSON parsing - Research query generation from architecture - Add Perplexity API client with native fetch - Retry logic with exponential backoff - Parse responses with sources and citations - Execute multiple research queries - Add prompt templates for architecture classification - Add default query generation as fallback - Add ArchitectureGenerator combining Claude + Perplexity - Add 44 new tests (80 total, all passing) All Phase 2 acceptance criteria met: - Claude client sends prompts and retries on failure - Perplexity client searches and parses with sources - Architecture classification returns valid JSON - Research queries generated (4-6 per architecture) Co-Authored-By: Claude Opus 4.5 --- .claude/ralph-loop.local.md | 51 +++--- prd.json | 8 +- progress.txt | 21 +++ src/__tests__/architecture.test.ts | 172 ++++++++++++++++++ src/__tests__/claude.test.ts | 199 +++++++++++++++++++++ src/__tests__/perplexity.test.ts | 235 +++++++++++++++++++++++++ src/__tests__/templates.test.ts | 235 +++++++++++++++++++++++++ src/clients/claude.ts | 274 +++++++++++++++++++++++++++++ src/clients/perplexity.ts | 232 ++++++++++++++++++++++++ src/generators/architecture.ts | 93 ++++++++++ src/prompts/templates.ts | 151 ++++++++++++++++ 11 files changed, 1643 insertions(+), 28 deletions(-) create mode 100644 src/__tests__/architecture.test.ts create mode 100644 src/__tests__/claude.test.ts create mode 100644 src/__tests__/perplexity.test.ts create mode 100644 src/__tests__/templates.test.ts create mode 100644 src/clients/claude.ts create mode 100644 src/clients/perplexity.ts create mode 100644 src/generators/architecture.ts create mode 100644 src/prompts/templates.ts diff --git a/.claude/ralph-loop.local.md b/.claude/ralph-loop.local.md index c9cb649..38942b5 100644 --- a/.claude/ralph-loop.local.md +++ b/.claude/ralph-loop.local.md @@ -1,37 +1,40 @@ --- active: true iteration: 1 -max_iterations: 30 -completion_promise: "PHASE_1_COMPLETE" -started_at: "2026-01-10T12:00:45Z" +max_iterations: 40 +completion_promise: "PHASE_2_COMPLETE" +started_at: "2026-01-10T12:10:57Z" --- -Execute Phase 1 - Foundation from PROMPT.md. +Execute Phase 2 - Core API Integration from PROMPT.md. -You are building the Ralph PRD Generator CLI tool. +Phase 1 is complete. Now implement Phase 2. -Read PROMPT.md for full specification. -Read prd.json for feature tracking. -Read CLAUDE.md for project configuration. +Read PROMPT.md for API specifications. +Read prd.json for Phase 2 features. -Phase 1 tasks: -1. Set up TypeScript project with all dependencies -2. Configure tsup, vitest, eslint -3. Implement CLI framework with Commander.js -4. Implement config management for API key storage -5. Create basic project structure -6. Write tests for config and CLI +Phase 2 tasks: +1. Claude API client with Anthropic SDK +2. Perplexity API client with fetch +3. Architecture classification prompt and parser +4. Research query generation +5. Tests for all API clients + +API Integration notes: +- Use environment variables for API keys during testing +- Implement retry logic with 3 attempts and exponential backoff +- Handle rate limits gracefully +- Parse JSON responses safely For each feature: -1. Write tests first +1. Write tests first using mocked API calls 2. Implement the feature -3. Run: npm run build && npm run test && npm run lint -4. If all pass, update prd.json by setting passes to true for completed features -5. Commit with descriptive message -6. Append progress to progress.txt +3. Verify: npm run build && npm run test && npm run lint +4. Update prd.json when feature passes +5. Commit and log progress -When ALL Phase 1 features in prd.json have passes set to true: -Output PHASE_1_COMPLETE +When ALL Phase 2 features pass: +Output PHASE_2_COMPLETE -If blocked after 10 attempts on same issue: -Document in progress.txt and output PHASE_1_BLOCKED +If blocked: +Output PHASE_2_BLOCKED diff --git a/prd.json b/prd.json index 6ff7e60..803f67e 100644 --- a/prd.json +++ b/prd.json @@ -26,7 +26,7 @@ "name": "Claude API Client", "description": "Anthropic SDK integration with retry logic", "priority": 1, - "passes": false, + "passes": true, "acceptance": "Can send prompt to Claude and receive response, retries on failure" }, { @@ -35,7 +35,7 @@ "name": "Perplexity API Client", "description": "Perplexity REST API integration", "priority": 2, - "passes": false, + "passes": true, "acceptance": "Can search Perplexity and parse response with sources" }, { @@ -44,7 +44,7 @@ "name": "Architecture Classification", "description": "Claude prompt for classifying app type", "priority": 3, - "passes": false, + "passes": true, "acceptance": "Given idea text, returns valid Architecture JSON" }, { @@ -53,7 +53,7 @@ "name": "Research Query Generation", "description": "Generate Perplexity queries from architecture", "priority": 4, - "passes": false, + "passes": true, "acceptance": "Given architecture, generates 4-6 relevant search queries" }, { diff --git a/progress.txt b/progress.txt index bb3109b..981c08b 100644 --- a/progress.txt +++ b/progress.txt @@ -18,3 +18,24 @@ * ralph-vibe init/new/validate/research exist * ralph-vibe new fails gracefully without keys +[2026-01-10T12:18:00Z] [2] [COMPLETE] - Phase 2 Core API Integration implemented + - Claude API client with Anthropic SDK + * Retry logic with exponential backoff (3 attempts) + * Handles auth errors, rate limits, server errors + * Architecture classification with JSON parsing + * Research query generation + - Perplexity API client with native fetch + * Retry logic with exponential backoff + * Response parsing with sources and citations + * Research queries execution + - Prompt templates for architecture classification + - Default query generation for fallback + - ArchitectureGenerator for full pipeline + - 80 tests passing, all lint checks pass + - All Phase 2 acceptance criteria met: + * Can send prompt to Claude and receive response + * Retries on failure with exponential backoff + * Can search Perplexity and parse response with sources + * Given idea text, returns valid Architecture JSON + * Given architecture, generates 4-6 relevant search queries + diff --git a/src/__tests__/architecture.test.ts b/src/__tests__/architecture.test.ts new file mode 100644 index 0000000..ca3f1b1 --- /dev/null +++ b/src/__tests__/architecture.test.ts @@ -0,0 +1,172 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { Config } from '../types/index.js'; + +// Create mock functions +const mockClassifyArchitecture = vi.fn(); +const mockGenerateResearchQueries = vi.fn(); +const mockResearchQueries = vi.fn(); + +// Mock the clients as classes +vi.mock('../clients/claude.js', () => ({ + ClaudeClient: class MockClaudeClient { + classifyArchitecture = mockClassifyArchitecture; + generateResearchQueries = mockGenerateResearchQueries; + }, +})); + +vi.mock('../clients/perplexity.js', () => ({ + PerplexityClient: class MockPerplexityClient { + researchQueries = mockResearchQueries; + }, +})); + +// Import after mocking +import { ArchitectureGenerator } from '../generators/architecture.js'; + +describe('ArchitectureGenerator', () => { + const mockConfig: Config = { + claudeApiKey: 'claude-test-key', + perplexityApiKey: 'perplexity-test-key', + }; + + const mockArchitecture = { + appType: 'cli' as const, + appTypeReason: 'Command-line tool', + interfaces: ['cli_args' as const], + interfacesReason: 'CLI arguments', + persistence: 'file_based' as const, + persistenceReason: 'File storage', + deployment: 'package_registry' as const, + deploymentReason: 'npm', + suggestedTechStack: { + language: 'TypeScript', + framework: 'Commander.js', + reasoning: 'CLI framework', + }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('throws error when Claude API key is missing', () => { + const configWithoutClaude: Config = { + perplexityApiKey: 'key', + }; + + expect(() => new ArchitectureGenerator({ config: configWithoutClaude })) + .toThrow('Claude API key is required'); + }); + + it('creates generator with valid config', () => { + expect(() => new ArchitectureGenerator({ config: mockConfig })) + .not.toThrow(); + }); + + it('creates generator without Perplexity when skipResearch is true', () => { + const configWithoutPerplexity: Config = { + claudeApiKey: 'key', + }; + + expect(() => new ArchitectureGenerator({ + config: configWithoutPerplexity, + skipResearch: true, + })).not.toThrow(); + }); + }); + + describe('classifyIdea', () => { + it('calls Claude client to classify idea', async () => { + mockClassifyArchitecture.mockResolvedValue(mockArchitecture); + + const generator = new ArchitectureGenerator({ config: mockConfig }); + const result = await generator.classifyIdea('A CLI tool for converting files'); + + expect(mockClassifyArchitecture).toHaveBeenCalledWith('A CLI tool for converting files'); + expect(result).toEqual(mockArchitecture); + }); + }); + + describe('generateResearchQueries', () => { + it('uses Claude to generate queries', async () => { + const mockQueries = ['query1', 'query2', 'query3', 'query4']; + mockGenerateResearchQueries.mockResolvedValue(mockQueries); + + const generator = new ArchitectureGenerator({ config: mockConfig }); + const result = await generator.generateResearchQueries(mockArchitecture); + + expect(mockGenerateResearchQueries).toHaveBeenCalledWith(mockArchitecture); + expect(result).toEqual(mockQueries); + }); + + it('falls back to default queries when Claude fails', async () => { + mockGenerateResearchQueries.mockRejectedValue(new Error('API error')); + + const generator = new ArchitectureGenerator({ config: mockConfig }); + const result = await generator.generateResearchQueries(mockArchitecture); + + // Should return default queries + expect(result.length).toBeGreaterThanOrEqual(4); + expect(result.some(q => q.toLowerCase().includes('cli'))).toBe(true); + }); + }); + + describe('conductResearch', () => { + it('uses Perplexity to research queries', async () => { + const mockResearch = { + queries: ['q1', 'q2'], + results: [ + { answer: 'Answer 1', sources: [], citations: [] }, + { answer: 'Answer 2', sources: [], citations: [] }, + ], + summary: 'Research summary', + }; + mockResearchQueries.mockResolvedValue(mockResearch); + + const generator = new ArchitectureGenerator({ config: mockConfig }); + const result = await generator.conductResearch(['q1', 'q2']); + + expect(mockResearchQueries).toHaveBeenCalledWith(['q1', 'q2']); + expect(result).toEqual(mockResearch); + }); + + it('returns empty research when Perplexity is not available', async () => { + const configWithoutPerplexity: Config = { + claudeApiKey: 'key', + }; + + const generator = new ArchitectureGenerator({ + config: configWithoutPerplexity, + skipResearch: true, + }); + + const result = await generator.conductResearch(['q1', 'q2']); + + expect(result.results).toHaveLength(0); + expect(result.summary).toBe('Research was skipped.'); + }); + }); + + describe('analyzeAndResearch', () => { + it('performs full analysis and research pipeline', async () => { + const mockQueries = ['query1', 'query2', 'query3', 'query4']; + const mockResearch = { + queries: mockQueries, + results: [{ answer: 'Research result', sources: [], citations: [] }], + summary: 'Summary', + }; + + mockClassifyArchitecture.mockResolvedValue(mockArchitecture); + mockGenerateResearchQueries.mockResolvedValue(mockQueries); + mockResearchQueries.mockResolvedValue(mockResearch); + + const generator = new ArchitectureGenerator({ config: mockConfig }); + const result = await generator.analyzeAndResearch('A CLI tool for files'); + + expect(result.architecture).toEqual(mockArchitecture); + expect(result.queries).toEqual(mockQueries); + expect(result.research).toEqual(mockResearch); + }); + }); +}); diff --git a/src/__tests__/claude.test.ts b/src/__tests__/claude.test.ts new file mode 100644 index 0000000..9b2ac67 --- /dev/null +++ b/src/__tests__/claude.test.ts @@ -0,0 +1,199 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Create a mock for the messages.create function +const mockCreate = vi.fn(); + +// Mock the Anthropic SDK as a class +vi.mock('@anthropic-ai/sdk', () => { + return { + default: class MockAnthropic { + messages = { + create: mockCreate, + }; + }, + }; +}); + +// Import after mocking +import { ClaudeClient } from '../clients/claude.js'; + +describe('ClaudeClient', () => { + let client: ClaudeClient; + + beforeEach(() => { + vi.clearAllMocks(); + client = new ClaudeClient({ apiKey: 'test-key' }); + }); + + describe('analyze', () => { + it('returns text response from Claude', async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: 'text', text: 'Hello, world!' }], + }); + + const result = await client.analyze('test prompt'); + expect(result).toBe('Hello, world!'); + }); + + it('throws error when no text block in response', async () => { + mockCreate.mockResolvedValueOnce({ + content: [], + }); + + await expect(client.analyze('test prompt')).rejects.toThrow('No text response from Claude'); + }); + + it('throws error for invalid API key', async () => { + const authError = new Error('Unauthorized') as Error & { status: number }; + authError.status = 401; + mockCreate.mockRejectedValueOnce(authError); + + await expect(client.analyze('test prompt')).rejects.toThrow('Invalid Claude API key'); + }); + + it('retries on server errors', async () => { + const serverError = new Error('Server Error') as Error & { status: number }; + serverError.status = 500; + + mockCreate + .mockRejectedValueOnce(serverError) + .mockRejectedValueOnce(serverError) + .mockResolvedValueOnce({ + content: [{ type: 'text', text: 'Success after retry' }], + }); + + const result = await client.analyze('test prompt'); + expect(result).toBe('Success after retry'); + expect(mockCreate).toHaveBeenCalledTimes(3); + }); + }); + + describe('classifyArchitecture', () => { + it('parses valid architecture JSON', async () => { + const architectureJson = JSON.stringify({ + appType: 'cli', + appTypeReason: 'Command-line tool', + interfaces: ['cli_args'], + interfacesReason: 'CLI arguments', + persistence: 'file_based', + persistenceReason: 'File storage', + deployment: 'package_registry', + deploymentReason: 'npm', + suggestedTechStack: { + language: 'TypeScript', + framework: 'Commander.js', + reasoning: 'CLI framework', + }, + }); + + mockCreate.mockResolvedValueOnce({ + content: [{ type: 'text', text: architectureJson }], + }); + + const result = await client.classifyArchitecture('A CLI tool for converting files'); + + expect(result.appType).toBe('cli'); + expect(result.interfaces).toContain('cli_args'); + expect(result.persistence).toBe('file_based'); + expect(result.deployment).toBe('package_registry'); + }); + + it('handles JSON wrapped in markdown code blocks', async () => { + const architectureJson = `\`\`\`json +{ + "appType": "web", + "appTypeReason": "Web app", + "interfaces": ["rest_api"], + "interfacesReason": "REST API", + "persistence": "remote_db", + "persistenceReason": "Database", + "deployment": "cloud", + "deploymentReason": "Cloud deployment", + "suggestedTechStack": { + "language": "TypeScript", + "framework": "Express", + "reasoning": "Node.js backend" + } +} +\`\`\``; + + mockCreate.mockResolvedValueOnce({ + content: [{ type: 'text', text: architectureJson }], + }); + + const result = await client.classifyArchitecture('A web application'); + + expect(result.appType).toBe('web'); + expect(result.interfaces).toContain('rest_api'); + }); + + it('throws error for invalid JSON', async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: 'text', text: 'not valid json' }], + }); + + await expect(client.classifyArchitecture('test')).rejects.toThrow('Failed to parse architecture'); + }); + + it('throws error for missing required fields', async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: 'text', text: '{"appType": "web"}' }], + }); + + await expect(client.classifyArchitecture('test')).rejects.toThrow('Missing required fields'); + }); + }); + + describe('generateResearchQueries', () => { + const mockArchitecture = { + appType: 'cli' as const, + appTypeReason: 'Command-line tool', + interfaces: ['cli_args' as const], + interfacesReason: 'CLI arguments', + persistence: 'file_based' as const, + persistenceReason: 'File storage', + deployment: 'package_registry' as const, + deploymentReason: 'npm', + suggestedTechStack: { + language: 'TypeScript', + framework: 'Commander.js', + reasoning: 'CLI framework', + }, + }; + + it('parses valid query array', async () => { + const queries = ['query1', 'query2', 'query3', 'query4']; + + mockCreate.mockResolvedValueOnce({ + content: [{ type: 'text', text: JSON.stringify(queries) }], + }); + + const result = await client.generateResearchQueries(mockArchitecture); + + expect(result).toEqual(queries); + expect(result.length).toBe(4); + }); + + it('handles JSON wrapped in markdown code blocks', async () => { + const queriesJson = `\`\`\`json +["query1", "query2", "query3", "query4"] +\`\`\``; + + mockCreate.mockResolvedValueOnce({ + content: [{ type: 'text', text: queriesJson }], + }); + + const result = await client.generateResearchQueries(mockArchitecture); + + expect(result).toEqual(['query1', 'query2', 'query3', 'query4']); + }); + + it('throws error for non-array response', async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: 'text', text: '{"not": "an array"}' }], + }); + + await expect(client.generateResearchQueries(mockArchitecture)).rejects.toThrow('not an array'); + }); + }); +}); diff --git a/src/__tests__/perplexity.test.ts b/src/__tests__/perplexity.test.ts new file mode 100644 index 0000000..8efff01 --- /dev/null +++ b/src/__tests__/perplexity.test.ts @@ -0,0 +1,235 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { PerplexityClient } from '../clients/perplexity.js'; + +describe('PerplexityClient', () => { + let client: PerplexityClient; + let mockFetch: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + mockFetch = vi.fn(); + global.fetch = mockFetch; + client = new PerplexityClient({ apiKey: 'test-key' }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('search', () => { + it('returns parsed search result', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 'test-id', + model: 'sonar-pro', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'This is the answer to your query.', + }, + finish_reason: 'stop', + }, + ], + citations: ['https://example.com/source1', 'https://example.com/source2'], + }), + }); + + const result = await client.search('What is TypeScript?'); + + expect(result.answer).toBe('This is the answer to your query.'); + expect(result.citations).toHaveLength(2); + expect(result.sources).toHaveLength(2); + expect(result.sources[0].url).toBe('https://example.com/source1'); + }); + + it('handles response without citations', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 'test-id', + model: 'sonar-pro', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'Answer without citations.', + }, + finish_reason: 'stop', + }, + ], + }), + }); + + const result = await client.search('Simple query'); + + expect(result.answer).toBe('Answer without citations.'); + expect(result.citations).toHaveLength(0); + expect(result.sources).toHaveLength(0); + }); + + it('throws error for invalid API key', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 401, + statusText: 'Unauthorized', + }); + + await expect(client.search('test')).rejects.toThrow('Invalid Perplexity API key'); + }); + + it('retries on server errors', async () => { + mockFetch + .mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + }) + .mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 'test-id', + model: 'sonar-pro', + choices: [ + { + index: 0, + message: { role: 'assistant', content: 'Success!' }, + finish_reason: 'stop', + }, + ], + }), + }); + + const result = await client.search('test'); + expect(result.answer).toBe('Success!'); + expect(mockFetch).toHaveBeenCalledTimes(3); + }); + + it('throws error when no choices in response', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 'test-id', + model: 'sonar-pro', + choices: [], + }), + }); + + await expect(client.search('test')).rejects.toThrow('No response from Perplexity'); + }); + + it('sends correct request format', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 'test-id', + model: 'sonar-pro', + choices: [ + { + index: 0, + message: { role: 'assistant', content: 'Response' }, + finish_reason: 'stop', + }, + ], + }), + }); + + await client.search('test query'); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://api.perplexity.ai/chat/completions', + expect.objectContaining({ + method: 'POST', + headers: { + 'Authorization': 'Bearer test-key', + 'Content-Type': 'application/json', + }, + }) + ); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(body.model).toBe('sonar-pro'); + expect(body.messages).toHaveLength(2); + expect(body.messages[1].content).toBe('test query'); + }); + }); + + describe('researchQueries', () => { + it('returns research with multiple query results', async () => { + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: '1', + model: 'sonar-pro', + choices: [{ index: 0, message: { role: 'assistant', content: 'Answer 1' }, finish_reason: 'stop' }], + citations: ['https://source1.com'], + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: '2', + model: 'sonar-pro', + choices: [{ index: 0, message: { role: 'assistant', content: 'Answer 2' }, finish_reason: 'stop' }], + citations: ['https://source2.com'], + }), + }); + + const queries = ['query1', 'query2']; + const result = await client.researchQueries(queries); + + expect(result.queries).toEqual(queries); + expect(result.results).toHaveLength(2); + expect(result.results[0].answer).toBe('Answer 1'); + expect(result.results[1].answer).toBe('Answer 2'); + expect(result.summary).toContain('Answer 1'); + expect(result.summary).toContain('Answer 2'); + }); + + it('continues when individual queries fail after retries', async () => { + // First query fails all 3 retries, second succeeds + mockFetch + .mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Error' }) + .mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Error' }) + .mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Error' }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: '2', + model: 'sonar-pro', + choices: [{ index: 0, message: { role: 'assistant', content: 'Success' }, finish_reason: 'stop' }], + }), + }); + + const result = await client.researchQueries(['fail-query', 'success-query']); + + expect(result.results).toHaveLength(1); + expect(result.results[0].answer).toBe('Success'); + }, 10000); + + it('returns empty results when all queries fail', async () => { + // All retries fail for both queries (3 retries each = 6 calls) + for (let i = 0; i < 6; i++) { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Error', + }); + } + + const result = await client.researchQueries(['fail1', 'fail2']); + + expect(result.results).toHaveLength(0); + expect(result.summary).toBe('No research results available.'); + }, 15000); + }); +}); diff --git a/src/__tests__/templates.test.ts b/src/__tests__/templates.test.ts new file mode 100644 index 0000000..f6db6c0 --- /dev/null +++ b/src/__tests__/templates.test.ts @@ -0,0 +1,235 @@ +import { describe, it, expect } from 'vitest'; +import { + buildArchitectureClassificationPrompt, + buildResearchQueryPrompt, + generateDefaultResearchQueries, +} from '../prompts/templates.js'; +import type { Architecture } from '../types/index.js'; + +describe('Prompt Templates', () => { + describe('buildArchitectureClassificationPrompt', () => { + it('includes the user idea in the prompt', () => { + const idea = 'A CLI tool that converts markdown to PDF'; + const prompt = buildArchitectureClassificationPrompt(idea); + + expect(prompt).toContain(''); + expect(prompt).toContain(idea); + expect(prompt).toContain(''); + }); + + it('includes all app type options', () => { + const prompt = buildArchitectureClassificationPrompt('test'); + + expect(prompt).toContain('web:'); + expect(prompt).toContain('desktop:'); + expect(prompt).toContain('cli:'); + expect(prompt).toContain('library:'); + expect(prompt).toContain('mobile:'); + expect(prompt).toContain('script:'); + }); + + it('includes all interface type options', () => { + const prompt = buildArchitectureClassificationPrompt('test'); + + expect(prompt).toContain('rest_api:'); + expect(prompt).toContain('graphql:'); + expect(prompt).toContain('ipc:'); + expect(prompt).toContain('cli_args:'); + expect(prompt).toContain('websocket:'); + }); + + it('includes all persistence options', () => { + const prompt = buildArchitectureClassificationPrompt('test'); + + expect(prompt).toContain('remote_db:'); + expect(prompt).toContain('local_db:'); + expect(prompt).toContain('file_based:'); + expect(prompt).toContain('in_memory:'); + }); + + it('includes all deployment options', () => { + const prompt = buildArchitectureClassificationPrompt('test'); + + expect(prompt).toContain('cloud:'); + expect(prompt).toContain('self_hosted:'); + expect(prompt).toContain('package_registry:'); + expect(prompt).toContain('app_store:'); + }); + + it('requests JSON output format', () => { + const prompt = buildArchitectureClassificationPrompt('test'); + + expect(prompt).toContain('Output ONLY valid JSON'); + expect(prompt).toContain('"appType"'); + expect(prompt).toContain('"interfaces"'); + expect(prompt).toContain('"persistence"'); + expect(prompt).toContain('"deployment"'); + }); + }); + + describe('buildResearchQueryPrompt', () => { + const mockArchitecture: Architecture = { + appType: 'cli', + appTypeReason: 'Command-line tool', + interfaces: ['cli_args'], + interfacesReason: 'CLI arguments', + persistence: 'file_based', + persistenceReason: 'File storage', + deployment: 'package_registry', + deploymentReason: 'npm', + suggestedTechStack: { + language: 'TypeScript', + framework: 'Commander.js', + reasoning: 'CLI framework', + }, + }; + + it('includes the architecture JSON', () => { + const prompt = buildResearchQueryPrompt(mockArchitecture); + + expect(prompt).toContain(''); + expect(prompt).toContain('"appType": "cli"'); + expect(prompt).toContain(''); + }); + + it('requests 4-6 search queries', () => { + const prompt = buildResearchQueryPrompt(mockArchitecture); + + expect(prompt).toContain('4-6 search queries'); + }); + + it('mentions key research areas', () => { + const prompt = buildResearchQueryPrompt(mockArchitecture); + + expect(prompt).toContain('best practices'); + expect(prompt).toContain('Common mistakes'); + expect(prompt).toContain('Recommended libraries'); + expect(prompt).toContain('Testing approaches'); + }); + + it('requests JSON array output', () => { + const prompt = buildResearchQueryPrompt(mockArchitecture); + + expect(prompt).toContain('JSON array of query strings'); + }); + }); + + describe('generateDefaultResearchQueries', () => { + it('generates queries for CLI apps', () => { + const architecture: Architecture = { + appType: 'cli', + appTypeReason: 'CLI tool', + interfaces: ['cli_args'], + interfacesReason: 'CLI', + persistence: 'file_based', + persistenceReason: 'Files', + deployment: 'package_registry', + deploymentReason: 'npm', + suggestedTechStack: { + language: 'TypeScript', + framework: 'Commander.js', + reasoning: 'CLI framework', + }, + }; + + const queries = generateDefaultResearchQueries(architecture); + + expect(queries.length).toBeGreaterThanOrEqual(4); + expect(queries.some(q => q.toLowerCase().includes('cli'))).toBe(true); + expect(queries.some(q => q.toLowerCase().includes('typescript'))).toBe(true); + }); + + it('generates queries for web apps', () => { + const architecture: Architecture = { + appType: 'web', + appTypeReason: 'Web app', + interfaces: ['rest_api'], + interfacesReason: 'REST', + persistence: 'remote_db', + persistenceReason: 'Database', + deployment: 'cloud', + deploymentReason: 'Cloud', + suggestedTechStack: { + language: 'TypeScript', + framework: 'Express', + reasoning: 'Backend', + }, + }; + + const queries = generateDefaultResearchQueries(architecture); + + expect(queries.length).toBeGreaterThanOrEqual(4); + expect(queries.some(q => q.toLowerCase().includes('web'))).toBe(true); + expect(queries.some(q => q.toLowerCase().includes('express'))).toBe(true); + }); + + it('generates queries for desktop apps', () => { + const architecture: Architecture = { + appType: 'desktop', + appTypeReason: 'Desktop app', + interfaces: ['ipc'], + interfacesReason: 'IPC', + persistence: 'local_db', + persistenceReason: 'SQLite', + deployment: 'desktop_installer', + deploymentReason: 'Installer', + suggestedTechStack: { + language: 'TypeScript', + framework: 'Tauri', + reasoning: 'Desktop framework', + }, + }; + + const queries = generateDefaultResearchQueries(architecture); + + expect(queries.length).toBeGreaterThanOrEqual(4); + expect(queries.some(q => q.toLowerCase().includes('tauri'))).toBe(true); + }); + + it('generates queries for library packages', () => { + const architecture: Architecture = { + appType: 'library', + appTypeReason: 'Library', + interfaces: ['module'], + interfacesReason: 'Module', + persistence: 'in_memory', + persistenceReason: 'None', + deployment: 'package_registry', + deploymentReason: 'npm', + suggestedTechStack: { + language: 'TypeScript', + framework: '', + reasoning: 'Library', + }, + }; + + const queries = generateDefaultResearchQueries(architecture); + + expect(queries.length).toBeGreaterThanOrEqual(4); + expect(queries.some(q => q.toLowerCase().includes('library'))).toBe(true); + }); + + it('includes current year in queries', () => { + const architecture: Architecture = { + appType: 'web', + appTypeReason: 'Web', + interfaces: ['rest_api'], + interfacesReason: 'REST', + persistence: 'remote_db', + persistenceReason: 'DB', + deployment: 'cloud', + deploymentReason: 'Cloud', + suggestedTechStack: { + language: 'TypeScript', + framework: 'Express', + reasoning: 'Backend', + }, + }; + + const queries = generateDefaultResearchQueries(architecture); + const currentYear = new Date().getFullYear().toString(); + + expect(queries.some(q => q.includes(currentYear))).toBe(true); + }); + }); +}); diff --git a/src/clients/claude.ts b/src/clients/claude.ts new file mode 100644 index 0000000..5b036fa --- /dev/null +++ b/src/clients/claude.ts @@ -0,0 +1,274 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { logger } from '../utils/logger.js'; +import type { Architecture, Specification, PRDOutput, ValidationResult, Research } from '../types/index.js'; + +const MAX_RETRIES = 3; +const INITIAL_BACKOFF_MS = 1000; + +export interface ClaudeClientOptions { + apiKey: string; + model?: string; +} + +export class ClaudeClient { + private client: Anthropic; + private model: string; + + constructor(options: ClaudeClientOptions) { + this.client = new Anthropic({ apiKey: options.apiKey }); + this.model = options.model || 'claude-sonnet-4-20250514'; + } + + private async retryWithBackoff( + operation: () => Promise, + operationName: string + ): Promise { + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + return await operation(); + } catch (err) { + const error = err as Error & { status?: number }; + lastError = error; + + // Don't retry on auth errors + if (error.status === 401) { + throw new Error('Invalid Claude API key. Run `ralph-vibe init` to configure.'); + } + + // Handle rate limits + if (error.status === 429) { + const waitTime = INITIAL_BACKOFF_MS * Math.pow(2, attempt); + logger.warn(`Rate limited. Waiting ${waitTime}ms before retry ${attempt}/${MAX_RETRIES}...`); + await this.sleep(waitTime); + continue; + } + + // Retry on server errors + if (error.status && error.status >= 500) { + const waitTime = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1); + logger.debug(`${operationName} failed (attempt ${attempt}/${MAX_RETRIES}): ${error.message}`); + if (attempt < MAX_RETRIES) { + await this.sleep(waitTime); + } + continue; + } + + // Don't retry on other errors + throw error; + } + } + + throw lastError || new Error(`${operationName} failed after ${MAX_RETRIES} attempts`); + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + async analyze(prompt: string): Promise { + return this.retryWithBackoff(async () => { + const response = await this.client.messages.create({ + model: this.model, + max_tokens: 4096, + messages: [{ role: 'user', content: prompt }], + }); + + const textBlock = response.content.find(block => block.type === 'text'); + if (!textBlock || textBlock.type !== 'text') { + throw new Error('No text response from Claude'); + } + + return textBlock.text; + }, 'analyze'); + } + + async classifyArchitecture(idea: string): Promise { + const prompt = `Analyze this app idea and classify its architecture: + + +${idea} + + +Determine: + +1. APPLICATION TYPE (choose one): + - web: Frontend + backend web application + - desktop: Native or Electron/Tauri desktop application + - cli: Command-line tool + - library: Reusable package/module + - mobile: iOS/Android application + - script: Automation/utility script + - other: Specify + +2. INTERFACE TYPES (choose all that apply): + - rest_api: RESTful HTTP endpoints + - graphql: GraphQL API + - ipc: Inter-process communication (desktop) + - module: Internal module boundaries (library) + - cli_args: Command-line arguments/flags + - file_format: Custom file format handling + - websocket: Real-time communication + - grpc: gRPC services + +3. PERSISTENCE (choose one): + - remote_db: Cloud/remote database (PostgreSQL, MySQL, MongoDB) + - local_db: Local database (SQLite, LevelDB) + - file_based: File system storage + - in_memory: No persistence + - cloud_storage: Object storage (S3, etc.) + +4. DEPLOYMENT (choose one): + - cloud: Cloud platform (Vercel, AWS, GCP, etc.) + - self_hosted: Self-managed server + - desktop_installer: Native installers + - package_registry: npm, PyPI, crates.io, etc. + - app_store: Mobile app stores + - none: No deployment (local script) + +Output ONLY valid JSON (no markdown, no explanation): +{ + "appType": "...", + "appTypeReason": "...", + "interfaces": ["..."], + "interfacesReason": "...", + "persistence": "...", + "persistenceReason": "...", + "deployment": "...", + "deploymentReason": "...", + "suggestedTechStack": { + "language": "...", + "framework": "...", + "reasoning": "..." + } +}`; + + const response = await this.analyze(prompt); + return this.parseArchitectureResponse(response); + } + + private parseArchitectureResponse(response: string): Architecture { + // Extract JSON from response (handle markdown code blocks) + let jsonStr = response.trim(); + + // Remove markdown code blocks if present + const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/); + if (jsonMatch) { + jsonStr = jsonMatch[1].trim(); + } + + try { + const parsed = JSON.parse(jsonStr); + + // Validate required fields + if (!parsed.appType || !parsed.interfaces || !parsed.persistence || !parsed.deployment) { + throw new Error('Missing required fields in architecture response'); + } + + return parsed as Architecture; + } catch (err) { + const error = err as Error; + logger.debug(`Failed to parse architecture response: ${response}`); + throw new Error(`Failed to parse architecture classification: ${error.message}`); + } + } + + async generateResearchQueries(architecture: Architecture): Promise { + const prompt = `Based on this architecture classification, generate Perplexity search queries +to research current best practices: + + +${JSON.stringify(architecture, null, 2)} + + +Generate 4-6 search queries that will find: +1. Project structure and architecture best practices for this stack +2. Common mistakes and pitfalls to avoid +3. Recommended libraries and tools (current year 2026) +4. Testing approaches and frameworks +5. (If applicable) Security considerations +6. (If applicable) Performance optimization + +Output ONLY a valid JSON array of query strings (no markdown, no explanation): +["query1", "query2", ...]`; + + const response = await this.analyze(prompt); + return this.parseQueriesResponse(response); + } + + private parseQueriesResponse(response: string): string[] { + let jsonStr = response.trim(); + + // Remove markdown code blocks if present + const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/); + if (jsonMatch) { + jsonStr = jsonMatch[1].trim(); + } + + try { + const parsed = JSON.parse(jsonStr); + + if (!Array.isArray(parsed)) { + throw new Error('Response is not an array'); + } + + if (parsed.length < 4 || parsed.length > 6) { + logger.warn(`Expected 4-6 queries, got ${parsed.length}`); + } + + return parsed.map(q => String(q)); + } catch (err) { + const error = err as Error; + logger.debug(`Failed to parse queries response: ${response}`); + throw new Error(`Failed to parse research queries: ${error.message}`); + } + } + + async generateSpec( + idea: string, + architecture: Architecture, + research: Research + ): Promise { + // This will be fully implemented in Phase 3 + const prompt = `Generate a specification for this application: + + +${idea} + + + +${JSON.stringify(architecture, null, 2)} + + + +${research.summary} + + +Output a specification with features, data models, interfaces, and tech stack as JSON.`; + + const response = await this.analyze(prompt); + + // Basic parsing - full implementation in Phase 3 + try { + let jsonStr = response.trim(); + const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/); + if (jsonMatch) { + jsonStr = jsonMatch[1].trim(); + } + return JSON.parse(jsonStr) as Specification; + } catch { + throw new Error('Failed to parse specification response'); + } + } + + async generatePRD(_spec: Specification): Promise { + // Placeholder - full implementation in Phase 3 + throw new Error('PRD generation not yet implemented'); + } + + async validate(_prompt: string): Promise { + // Placeholder - can use Claude for advanced validation in Phase 4 + throw new Error('Claude-based validation not yet implemented'); + } +} diff --git a/src/clients/perplexity.ts b/src/clients/perplexity.ts new file mode 100644 index 0000000..f3f2015 --- /dev/null +++ b/src/clients/perplexity.ts @@ -0,0 +1,232 @@ +import { logger } from '../utils/logger.js'; +import type { SearchResult, Research, TechStack } from '../types/index.js'; + +const MAX_RETRIES = 3; +const INITIAL_BACKOFF_MS = 1000; +const API_BASE_URL = 'https://api.perplexity.ai'; + +export interface PerplexityClientOptions { + apiKey: string; + model?: string; +} + +interface PerplexityMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +interface PerplexityResponse { + id: string; + model: string; + choices: { + index: number; + message: { + role: string; + content: string; + }; + finish_reason: string; + }[]; + citations?: string[]; +} + +export class PerplexityClient { + private apiKey: string; + private model: string; + + constructor(options: PerplexityClientOptions) { + this.apiKey = options.apiKey; + this.model = options.model || 'sonar-pro'; + } + + private async retryWithBackoff( + operation: () => Promise, + operationName: string + ): Promise { + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + return await operation(); + } catch (err) { + const error = err as Error & { status?: number }; + lastError = error; + + // Don't retry on auth errors + if (error.status === 401) { + throw new Error('Invalid Perplexity API key. Run `ralph-vibe init` to configure.'); + } + + // Handle rate limits + if (error.status === 429) { + const waitTime = INITIAL_BACKOFF_MS * Math.pow(2, attempt); + logger.warn(`Rate limited. Waiting ${waitTime}ms before retry ${attempt}/${MAX_RETRIES}...`); + await this.sleep(waitTime); + continue; + } + + // Retry on server errors + if (error.status && error.status >= 500) { + const waitTime = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1); + logger.debug(`${operationName} failed (attempt ${attempt}/${MAX_RETRIES}): ${error.message}`); + if (attempt < MAX_RETRIES) { + await this.sleep(waitTime); + } + continue; + } + + // Don't retry on other errors + throw error; + } + } + + throw lastError || new Error(`${operationName} failed after ${MAX_RETRIES} attempts`); + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + async search(query: string): Promise { + return this.retryWithBackoff(async () => { + const messages: PerplexityMessage[] = [ + { + role: 'system', + content: 'You are a helpful research assistant. Provide concise, factual answers with sources.', + }, + { + role: 'user', + content: query, + }, + ]; + + const response = await fetch(`${API_BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: this.model, + messages, + max_tokens: 2048, + }), + }); + + if (!response.ok) { + const error = new Error(`Perplexity API error: ${response.status} ${response.statusText}`) as Error & { status: number }; + error.status = response.status; + throw error; + } + + const data = await response.json() as PerplexityResponse; + + if (!data.choices || data.choices.length === 0) { + throw new Error('No response from Perplexity'); + } + + const answer = data.choices[0].message.content; + const citations = data.citations || []; + + // Parse sources from citations + const sources = citations.map((url, index) => ({ + title: `Source ${index + 1}`, + url, + })); + + return { + answer, + sources, + citations, + }; + }, 'search'); + } + + async researchStack(stack: TechStack): Promise { + const queries = this.generateStackQueries(stack); + const results: SearchResult[] = []; + + for (const query of queries) { + logger.debug(`Researching: ${query}`); + try { + const result = await this.search(query); + results.push(result); + } catch (err) { + const error = err as Error; + logger.warn(`Research query failed: ${error.message}`); + // Continue with other queries + } + } + + // Generate summary from all results + const summary = this.summarizeResults(results); + + return { + queries, + results, + summary, + }; + } + + async researchQueries(queries: string[]): Promise { + const results: SearchResult[] = []; + + for (const query of queries) { + logger.debug(`Researching: ${query}`); + try { + const result = await this.search(query); + results.push(result); + } catch (err) { + const error = err as Error; + logger.warn(`Research query failed: ${error.message}`); + // Continue with other queries + } + } + + // Generate summary from all results + const summary = this.summarizeResults(results); + + return { + queries, + results, + summary, + }; + } + + private generateStackQueries(stack: TechStack): string[] { + const year = new Date().getFullYear(); + const queries: string[] = []; + + // Best practices query + if (stack.framework) { + queries.push(`${stack.language} ${stack.framework} project structure best practices ${year}`); + } else { + queries.push(`${stack.language} project structure best practices ${year}`); + } + + // Common mistakes query + queries.push(`${stack.language} ${stack.framework || ''} common mistakes pitfalls to avoid`.trim()); + + // Recommended libraries query + queries.push(`${stack.language} recommended libraries ${year} ${stack.libraries.slice(0, 2).join(' ')}`.trim()); + + // Testing query + queries.push(`${stack.language} ${stack.testingFramework} testing best practices ${year}`); + + return queries; + } + + private summarizeResults(results: SearchResult[]): string { + if (results.length === 0) { + return 'No research results available.'; + } + + const summaryParts = results.map((result, index) => { + const sources = result.sources.length > 0 + ? `\n Sources: ${result.sources.map(s => s.url).join(', ')}` + : ''; + return `### Finding ${index + 1}\n${result.answer}${sources}`; + }); + + return summaryParts.join('\n\n'); + } +} diff --git a/src/generators/architecture.ts b/src/generators/architecture.ts new file mode 100644 index 0000000..ce57a29 --- /dev/null +++ b/src/generators/architecture.ts @@ -0,0 +1,93 @@ +import { ClaudeClient } from '../clients/claude.js'; +import { PerplexityClient } from '../clients/perplexity.js'; +import { generateDefaultResearchQueries } from '../prompts/templates.js'; +import { logger } from '../utils/logger.js'; +import type { Architecture, Research, Config } from '../types/index.js'; + +export interface ArchitectureGeneratorOptions { + config: Config; + skipResearch?: boolean; +} + +export class ArchitectureGenerator { + private claudeClient: ClaudeClient; + private perplexityClient: PerplexityClient | null; + + constructor(options: ArchitectureGeneratorOptions) { + if (!options.config.claudeApiKey) { + throw new Error('Claude API key is required'); + } + + this.claudeClient = new ClaudeClient({ + apiKey: options.config.claudeApiKey, + }); + + if (options.config.perplexityApiKey && !options.skipResearch) { + this.perplexityClient = new PerplexityClient({ + apiKey: options.config.perplexityApiKey, + }); + } else { + this.perplexityClient = null; + } + } + + async classifyIdea(idea: string): Promise { + logger.info('Analyzing app idea...'); + const architecture = await this.claudeClient.classifyArchitecture(idea); + logger.success('Architecture classification complete'); + return architecture; + } + + async generateResearchQueries(architecture: Architecture): Promise { + logger.info('Generating research queries...'); + + try { + // Try to use Claude to generate smart queries + const queries = await this.claudeClient.generateResearchQueries(architecture); + logger.success(`Generated ${queries.length} research queries`); + return queries; + } catch (err) { + const error = err as Error; + logger.warn(`Claude query generation failed: ${error.message}`); + logger.info('Using default research queries...'); + + // Fall back to default queries + const queries = generateDefaultResearchQueries(architecture); + logger.success(`Generated ${queries.length} default research queries`); + return queries; + } + } + + async conductResearch(queries: string[]): Promise { + if (!this.perplexityClient) { + logger.warn('Perplexity client not available, skipping research'); + return { + queries, + results: [], + summary: 'Research was skipped.', + }; + } + + logger.info(`Conducting research with ${queries.length} queries...`); + const research = await this.perplexityClient.researchQueries(queries); + logger.success(`Research complete with ${research.results.length} findings`); + return research; + } + + async analyzeAndResearch(idea: string): Promise<{ + architecture: Architecture; + queries: string[]; + research: Research; + }> { + // Step 1: Classify the idea + const architecture = await this.classifyIdea(idea); + + // Step 2: Generate research queries + const queries = await this.generateResearchQueries(architecture); + + // Step 3: Conduct research + const research = await this.conductResearch(queries); + + return { architecture, queries, research }; + } +} diff --git a/src/prompts/templates.ts b/src/prompts/templates.ts new file mode 100644 index 0000000..69e1532 --- /dev/null +++ b/src/prompts/templates.ts @@ -0,0 +1,151 @@ +import type { Architecture } from '../types/index.js'; + +export const ARCHITECTURE_CLASSIFICATION_PROMPT = `Analyze this app idea and classify its architecture: + + +{user_idea} + + +Determine: + +1. APPLICATION TYPE (choose one): + - web: Frontend + backend web application + - desktop: Native or Electron/Tauri desktop application + - cli: Command-line tool + - library: Reusable package/module + - mobile: iOS/Android application + - script: Automation/utility script + - other: Specify + +2. INTERFACE TYPES (choose all that apply): + - rest_api: RESTful HTTP endpoints + - graphql: GraphQL API + - ipc: Inter-process communication (desktop) + - module: Internal module boundaries (library) + - cli_args: Command-line arguments/flags + - file_format: Custom file format handling + - websocket: Real-time communication + - grpc: gRPC services + +3. PERSISTENCE (choose one): + - remote_db: Cloud/remote database (PostgreSQL, MySQL, MongoDB) + - local_db: Local database (SQLite, LevelDB) + - file_based: File system storage + - in_memory: No persistence + - cloud_storage: Object storage (S3, etc.) + +4. DEPLOYMENT (choose one): + - cloud: Cloud platform (Vercel, AWS, GCP, etc.) + - self_hosted: Self-managed server + - desktop_installer: Native installers + - package_registry: npm, PyPI, crates.io, etc. + - app_store: Mobile app stores + - none: No deployment (local script) + +Output ONLY valid JSON (no markdown, no explanation): +{ + "appType": "...", + "appTypeReason": "...", + "interfaces": ["..."], + "interfacesReason": "...", + "persistence": "...", + "persistenceReason": "...", + "deployment": "...", + "deploymentReason": "...", + "suggestedTechStack": { + "language": "...", + "framework": "...", + "reasoning": "..." + } +}`; + +export function buildArchitectureClassificationPrompt(idea: string): string { + return ARCHITECTURE_CLASSIFICATION_PROMPT.replace('{user_idea}', idea); +} + +export const RESEARCH_QUERY_GENERATION_PROMPT = `Based on this architecture classification, generate Perplexity search queries +to research current best practices: + + +{architecture_json} + + +Generate 4-6 search queries that will find: +1. Project structure and architecture best practices for this stack +2. Common mistakes and pitfalls to avoid +3. Recommended libraries and tools (current year) +4. Testing approaches and frameworks +5. (If applicable) Security considerations +6. (If applicable) Performance optimization + +Output ONLY a valid JSON array of query strings (no markdown, no explanation): +["query1", "query2", ...]`; + +export function buildResearchQueryPrompt(architecture: Architecture): string { + return RESEARCH_QUERY_GENERATION_PROMPT.replace( + '{architecture_json}', + JSON.stringify(architecture, null, 2) + ); +} + +export function generateDefaultResearchQueries(architecture: Architecture): string[] { + const { appType, suggestedTechStack } = architecture; + const year = new Date().getFullYear(); + const lang = suggestedTechStack.language; + const framework = suggestedTechStack.framework; + + const queries: string[] = []; + + // App type specific queries + switch (appType) { + case 'web': + queries.push(`${lang} ${framework} web application best practices ${year}`); + queries.push(`${framework} common security mistakes to avoid`); + queries.push(`${lang} web backend recommended libraries ${year} authentication database`); + queries.push(`${lang} ${framework} testing Jest Vitest comparison ${year}`); + break; + + case 'desktop': + queries.push(`${framework} project structure best practices`); + queries.push(`${framework} common mistakes`); + queries.push(`${framework} recommended plugins`); + queries.push(`${framework} application testing approaches`); + break; + + case 'cli': + queries.push(`${lang} CLI tool best practices argument parsing`); + queries.push(`CLI tool UX common mistakes`); + queries.push(`${lang} CLI framework comparison ${year}`); + queries.push(`CLI tool testing strategies`); + break; + + case 'library': + queries.push(`${lang} library package best practices ${year}`); + queries.push(`${lang} npm package common mistakes`); + queries.push(`${lang} library testing and documentation ${year}`); + queries.push(`${lang} package bundling and tree shaking`); + break; + + case 'mobile': + queries.push(`${framework} mobile app best practices ${year}`); + queries.push(`${framework} common performance mistakes`); + queries.push(`${framework} recommended libraries state management ${year}`); + queries.push(`${framework} mobile testing approaches`); + break; + + case 'script': + queries.push(`${lang} automation script best practices`); + queries.push(`${lang} scripting common mistakes error handling`); + queries.push(`${lang} scripting libraries utilities ${year}`); + queries.push(`${lang} script testing`); + break; + + default: + queries.push(`${lang} ${framework} best practices ${year}`); + queries.push(`${lang} common mistakes to avoid`); + queries.push(`${lang} recommended libraries ${year}`); + queries.push(`${lang} testing frameworks ${year}`); + } + + return queries; +}