import { NextRequest, NextResponse } from 'next/server'; import admin from '@/lib/firebase/admin'; import { GoogleGenerativeAI } from '@google/generative-ai'; import { getApiUrl } from '@/lib/utils/api-url'; import fs from 'fs'; import path from 'path'; /** * MVP Page & Feature Checklist Generator (AI-Powered) * Uses Gemini AI with the Vibn MVP Planner agent spec to generate intelligent, * context-aware plans from project vision answers and existing work */ export async function GET( request: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; const db = admin.firestore(); // Check if we have a saved plan const projectDoc = await db.collection('projects').doc(projectId).get(); const projectData = projectDoc.data(); if (projectData?.mvpChecklist && !request.nextUrl.searchParams.get('regenerate')) { console.log('Loading saved MVP checklist'); return NextResponse.json({ ...projectData.mvpChecklist, cached: true, cachedAt: projectData.mvpChecklistGeneratedAt }); } // If no checklist exists and not forcing regeneration, return empty state if (!projectData?.mvpChecklist && !request.nextUrl.searchParams.get('regenerate')) { console.log('[MVP Generation] No checklist exists - returning empty state'); return NextResponse.json({ error: 'No MVP checklist generated yet', message: 'Click "Regenerate Plan" to create your MVP checklist', mvpChecklist: [], summary: { totalPages: 0, estimatedDays: 0 } }); } console.log('[MVP Generation] 🚀 Starting MVP checklist generation...'); // Load complete history console.log('[MVP Generation] 📊 Loading project history...'); const historyResponse = await fetch( getApiUrl(`/api/projects/${projectId}/complete-history`, request) ); const history = await historyResponse.json(); console.log('[MVP Generation] ✅ History loaded'); // Load intelligent analysis (with fallback if project doesn't have codebase access) console.log('[MVP Generation] 🧠 Running intelligent analysis...'); let analysis = null; try { const analysisResponse = await fetch( getApiUrl(`/api/projects/${projectId}/plan/intelligent`, request) ); if (analysisResponse.ok) { analysis = await analysisResponse.json(); console.log('[MVP Generation] ✅ Analysis complete'); } else { console.log('[MVP Generation] ⚠️ Analysis failed (project may lack codebase access), using fallback'); analysis = { codebaseAnalysis: null, intelligentPlan: null }; } } catch (error) { console.log('[MVP Generation] ⚠️ Analysis error:', error instanceof Error ? error.message : String(error)); analysis = { codebaseAnalysis: null, intelligentPlan: null }; } // Generate MVP checklist using AI console.log('[MVP Generation] 🤖 Calling AI to generate MVP plan...'); const checklist = await generateAIMVPChecklist(projectId, history, analysis, projectData); console.log('[MVP Generation] ✅ MVP plan generated!'); // Save to Firestore (filter out undefined values to avoid Firestore errors) const cleanChecklist = JSON.parse(JSON.stringify(checklist, (key, value) => value === undefined ? null : value )); await db.collection('projects').doc(projectId).update({ mvpChecklist: cleanChecklist, mvpChecklistGeneratedAt: admin.firestore.FieldValue.serverTimestamp() }); console.log('[MVP Generation] ✅ MVP checklist saved to Firestore'); return NextResponse.json(checklist); } catch (error) { console.error('Error generating MVP checklist:', error); return NextResponse.json( { error: 'Failed to generate MVP checklist', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } /** * POST to force regeneration of the checklist */ export async function POST( request: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; const db = admin.firestore(); console.log('[MVP Generation] 🚀 Starting MVP checklist regeneration...'); // Re-fetch project data const projectDoc = await db.collection('projects').doc(projectId).get(); const projectData = projectDoc.data(); // Load complete history console.log('[MVP Generation] 📊 Loading project history...'); const historyResponse = await fetch( getApiUrl(`/api/projects/${projectId}/complete-history`, request) ); const history = await historyResponse.json(); console.log('[MVP Generation] ✅ History loaded'); // Load intelligent analysis (with fallback if project doesn't have codebase access) console.log('[MVP Generation] 🧠 Running intelligent analysis...'); let analysis = null; try { const analysisResponse = await fetch( getApiUrl(`/api/projects/${projectId}/plan/intelligent`, request) ); if (analysisResponse.ok) { analysis = await analysisResponse.json(); console.log('[MVP Generation] ✅ Analysis complete'); } else { console.log('[MVP Generation] ⚠️ Analysis failed (project may lack codebase access), using fallback'); analysis = { codebaseAnalysis: null, intelligentPlan: null }; } } catch (error) { console.log('[MVP Generation] ⚠️ Analysis error:', error instanceof Error ? error.message : String(error)); analysis = { codebaseAnalysis: null, intelligentPlan: null }; } // Generate MVP checklist using AI console.log('[MVP Generation] 🤖 Calling AI to generate MVP plan...'); const checklist = await generateAIMVPChecklist(projectId, history, analysis, projectData); console.log('[MVP Generation] ✅ MVP plan generated!'); console.log('[MVP Generation] 📊 Summary:', JSON.stringify(checklist.summary, null, 2)); // Save to Firestore (filter out undefined values to avoid Firestore errors) const cleanChecklist = JSON.parse(JSON.stringify(checklist, (key, value) => value === undefined ? null : value )); await db.collection('projects').doc(projectId).update({ mvpChecklist: cleanChecklist, mvpChecklistGeneratedAt: admin.firestore.FieldValue.serverTimestamp() }); console.log('[MVP Generation] ✅ MVP checklist saved to Firestore'); return NextResponse.json({ ...checklist, regenerated: true }); } catch (error) { console.error('[MVP Generation] ❌ Error regenerating MVP checklist:', error); return NextResponse.json( { error: 'Failed to regenerate MVP checklist', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } /** * Generate AI-powered MVP Checklist using Gemini and the Vibn MVP Planner agent spec */ async function generateAIMVPChecklist( projectId: string, history: any, analysis: any, projectData: any ) { try { // Check for Gemini API key const geminiApiKey = process.env.GEMINI_API_KEY; if (!geminiApiKey) { console.warn('[MVP Generation] ⚠️ No GEMINI_API_KEY found, falling back to template-based generation'); return generateFallbackChecklist(history, analysis); } console.log('[MVP Generation] 🔑 GEMINI_API_KEY found, using AI generation'); // Load the agent spec const agentSpecPath = path.join(process.cwd(), '..', 'vibn-vision', 'initial-questions.json'); const agentSpec = JSON.parse(fs.readFileSync(agentSpecPath, 'utf-8')); console.log('[MVP Generation] 📋 Agent spec loaded'); // Initialize Gemini const genAI = new GoogleGenerativeAI(geminiApiKey); const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash-exp", generationConfig: { temperature: 0.4, topP: 0.95, topK: 40, maxOutputTokens: 8192, responseMimeType: "application/json", }, }); console.log('[MVP Generation] 🤖 Gemini model initialized (gemini-2.0-flash-exp)'); // Prepare vision input from project data const visionInput = prepareVisionInput(projectData, history); console.log('[MVP Generation] 📝 Vision input prepared:', { q1: visionInput.q1_who_and_problem.raw_answer?.substring(0, 50) + '...', q2: visionInput.q2_story.raw_answer?.substring(0, 50) + '...', q3: visionInput.q3_improvement.raw_answer?.substring(0, 50) + '...' }); // Log what data we have vs missing console.log('[MVP Generation] 📊 Data availability check:'); console.log(' ✅ Vision answers:', !!projectData.visionAnswers); console.log(' ✅ GitHub repo:', projectData.githubRepo || 'None'); console.log(' ⚠️ GitHub userId:', projectData.userId || 'MISSING - cannot load repo code'); console.log(' ✅ Git commits:', history.gitSummary?.totalCommits || 0); console.log(' ✅ Cursor sessions:', history.summary?.breakdown?.extensionSessions || 0); console.log(' ✅ Codebase analysis:', analysis.codebaseAnalysis?.builtFeatures?.length || 0, 'features found'); // Load Cursor conversation history from Firestore console.log('[MVP Generation] 💬 Loading Cursor conversation history...'); const adminDb = admin.firestore(); let cursorConversations: any[] = []; let cursorMessageCount = 0; try { const conversationsSnapshot = await adminDb .collection('projects') .doc(projectId) .collection('cursorConversations') .orderBy('lastUpdatedAt', 'desc') .limit(10) // Get most recent 10 conversations .get(); for (const convDoc of conversationsSnapshot.docs) { const convData = convDoc.data(); const messagesSnapshot = await adminDb .collection('projects') .doc(projectId) .collection('cursorConversations') .doc(convDoc.id) .collection('messages') .orderBy('createdAt', 'asc') .limit(50) // Limit messages per conversation to avoid token bloat .get(); const messages = messagesSnapshot.docs.map(msgDoc => { const msg = msgDoc.data(); return { role: msg.type === 1 ? 'user' : 'assistant', text: msg.text || '', createdAt: msg.createdAt }; }); cursorMessageCount += messages.length; cursorConversations.push({ name: convData.name || 'Untitled', messageCount: messages.length, messages: messages, createdAt: convData.createdAt, lastUpdatedAt: convData.lastUpdatedAt }); } console.log('[MVP Generation] ✅ Loaded', cursorConversations.length, 'Cursor conversations with', cursorMessageCount, 'messages'); } catch (error) { console.error('[MVP Generation] ⚠️ Failed to load Cursor conversations:', error); } // Prepare work_to_date context with all available data const githubSummary = history.gitSummary ? `${history.gitSummary.totalCommits || 0} commits, ${history.gitSummary.filesChanged || 0} files changed` : 'No Git history available'; const codebaseSummary = analysis.codebaseAnalysis?.summary || (analysis.codebaseAnalysis?.builtFeatures?.length > 0 ? `Built: ${analysis.codebaseAnalysis.builtFeatures.map((f: any) => f.name).join(', ')}` : 'No codebase analysis available'); const cursorSessionsSummary = cursorConversations.length > 0 ? `${cursorConversations.length} Cursor conversations with ${cursorMessageCount} messages imported from Cursor IDE` : 'No Cursor conversation history available'; // Format Cursor conversations for the prompt const cursorContextText = cursorConversations.length > 0 ? cursorConversations.map(conv => `Conversation: "${conv.name}" (${conv.messageCount} messages)\n` + conv.messages.slice(0, 10).map((m: any) => ` ${m.role}: ${m.text.substring(0, 200)}`).join('\n') ).join('\n\n') : ''; const workToDate = { code_summary: codebaseSummary, github_summary: githubSummary, cursor_sessions_summary: cursorSessionsSummary, cursor_conversations: cursorContextText, // Include actual conversation snippets existing_assets_notes: `Built features: ${analysis.codebaseAnalysis?.builtFeatures?.length || 0}, Missing: ${analysis.codebaseAnalysis?.missingFeatures?.length || 0}` }; console.log('[MVP Generation] 🔍 Work context prepared:', { ...workToDate, cursor_conversations: cursorContextText.length > 0 ? `${cursorContextText.length} chars from conversations` : 'None' }); // Build the prompt with agent spec instructions const prompt = `${agentSpec.agent_spec.instructions_for_model} Here is the input data: ${JSON.stringify({ vision_input: visionInput, work_to_date: workToDate }, null, 2)} Return ONLY valid JSON matching the output schema, with no additional text or markdown.`; console.log('[MVP Generation] 📤 Sending prompt to Gemini (length:', prompt.length, 'chars)'); // Call Gemini const result = await model.generateContent(prompt); const response = result.response; const text = response.text(); console.log('[MVP Generation] 📥 Received AI response (length:', text.length, 'chars)'); // Parse AI response (Gemini returns JSON directly with responseMimeType set) const aiResponse = JSON.parse(text); console.log('[MVP Generation] ✅ AI response parsed successfully'); console.log('[MVP Generation] 🔍 AI Response structure:', JSON.stringify({ has_journey_tree: !!aiResponse.journey_tree, has_touchpoints_tree: !!aiResponse.touchpoints_tree, has_system_tree: !!aiResponse.system_tree, journey_nodes: aiResponse.journey_tree?.nodes?.length || 0, touchpoints_nodes: aiResponse.touchpoints_tree?.nodes?.length || 0, system_nodes: aiResponse.system_tree?.nodes?.length || 0, summary: aiResponse.summary }, null, 2)); // Transform AI trees into our existing format const checklist = transformAIResponseToChecklist(aiResponse, history, analysis); console.log('[MVP Generation] ✅ Checklist transformed, total pages:', checklist.summary?.totalPages || 0); return checklist; } catch (error) { console.error('[MVP Generation] ❌ Error generating AI MVP checklist:', error); console.warn('[MVP Generation] ⚠️ Falling back to template-based generation'); return generateFallbackChecklist(history, analysis); } } /** * Fallback to template-based generation if AI fails */ function generateFallbackChecklist(history: any, analysis: any) { const vision = history.project.vision || ''; const builtFeatures = analysis.codebaseAnalysis?.builtFeatures || []; const missingFeatures = analysis.codebaseAnalysis?.missingFeatures || []; // Scan commit messages for evidence of pages const commitMessages = history.chronologicalEvents .filter((e: any) => e.type === 'git_commit') .map((e: any) => e.data.message); // Simple flat taxonomy structure (existing template) const corePages = [ { category: 'Core Features', pages: [ { path: '/auth', title: 'Authentication', status: detectPageStatus('auth', commitMessages, builtFeatures), priority: 'critical', evidence: findEvidence('auth', commitMessages) }, { path: '/[workspace]', title: 'Workspace Selector', status: detectPageStatus('workspace', commitMessages, builtFeatures), priority: 'critical', evidence: findEvidence('workspace', commitMessages) }, { path: '/[workspace]/projects', title: 'Projects List', status: detectPageStatus('projects page', commitMessages, builtFeatures), priority: 'critical', evidence: findEvidence('projects list', commitMessages) }, { path: '/project/[id]/overview', title: 'Project Dashboard', status: detectPageStatus('overview', commitMessages, builtFeatures), priority: 'critical', evidence: findEvidence('overview', commitMessages) }, { path: '/project/[id]/mission', title: 'Vision/Mission Screen', status: detectPageStatus('mission|vision', commitMessages, builtFeatures), priority: 'critical', evidence: findEvidence('vision|mission', commitMessages) }, { path: '/project/[id]/audit', title: 'Project History & Audit', status: detectPageStatus('audit', commitMessages, builtFeatures), priority: 'high', evidence: findEvidence('audit', commitMessages) }, { path: '/project/[id]/timeline-plan', title: 'MVP Timeline & Checklist', status: detectPageStatus('timeline-plan', commitMessages, builtFeatures), priority: 'critical', evidence: findEvidence('timeline-plan', commitMessages) }, { path: '/api/github/oauth', title: 'GitHub OAuth API', status: detectPageStatus('github/oauth', commitMessages, builtFeatures), priority: 'critical', evidence: findEvidence('github oauth', commitMessages) }, { path: '/api/projects', title: 'Project Management APIs', status: detectPageStatus('api/projects', commitMessages, builtFeatures), priority: 'critical', evidence: findEvidence('project api', commitMessages) }, { path: '/api/projects/[id]/mvp-checklist', title: 'MVP Checklist Generation API', status: detectPageStatus('mvp-checklist', commitMessages, builtFeatures), priority: 'critical', evidence: findEvidence('mvp-checklist', commitMessages) } ] }, { category: 'Flows', pages: [ { path: 'flow/onboarding', title: 'User Onboarding Flow', status: 'in_progress', priority: 'critical', evidence: [], note: 'Sign Up → Workspace Creation → Connect GitHub' }, { path: 'flow/project-creation', title: 'Project Creation Flow', status: 'in_progress', priority: 'critical', evidence: findEvidence('project creation', commitMessages), note: 'Import/New Project → Repository → History Import → Vision Setup' }, { path: 'flow/plan-generation', title: 'Plan Generation Flow', status: 'in_progress', priority: 'critical', evidence: findEvidence('plan', commitMessages), note: 'Context Analysis → MVP Checklist → Timeline View' } ] }, { category: 'Marketing', pages: [ { path: '/project/[id]/marketing', title: 'Marketing Dashboard', status: 'missing', priority: 'high', evidence: [], note: 'Have /plan/marketing API but no UI' }, { path: '/api/projects/[id]/plan/marketing', title: 'Marketing Plan Generation API', status: detectPageStatus('marketing api', commitMessages, builtFeatures), priority: 'high', evidence: findEvidence('marketing', commitMessages) }, { path: '/', title: 'Marketing Landing Page', status: detectPageStatus('marketing page', commitMessages, builtFeatures), priority: 'high', evidence: findEvidence('marketing site|landing', commitMessages) } ] }, { category: 'Social', pages: [ { path: '/[workspace]/connections', title: 'Social Connections & Integrations', status: detectPageStatus('connections', commitMessages, builtFeatures), priority: 'medium', evidence: findEvidence('connections', commitMessages) } ] }, { category: 'Content', pages: [ { path: '/docs', title: 'Documentation Pages', status: 'missing', priority: 'medium', evidence: [] }, { path: '/project/[id]/getting-started', title: 'Getting Started Guide', status: detectPageStatus('getting-started', commitMessages, builtFeatures), priority: 'medium', evidence: findEvidence('getting-started|onboarding', commitMessages) } ] }, { category: 'Settings', pages: [ { path: '/project/[id]/settings', title: 'Project Settings', status: detectPageStatus('settings', commitMessages, builtFeatures), priority: 'high', evidence: findEvidence('settings', commitMessages) }, { path: '/[workspace]/settings', title: 'User Settings', status: detectPageStatus('settings', commitMessages, builtFeatures), priority: 'medium', evidence: findEvidence('settings', commitMessages) } ] } ]; // Calculate statistics const allPages = corePages.flatMap(c => c.pages); const builtCount = allPages.filter(p => p.status === 'built').length; const inProgressCount = allPages.filter(p => p.status === 'in_progress').length; const missingCount = allPages.filter(p => p.status === 'missing').length; return { project: { name: history.project.name, vision: history.project.vision, githubRepo: history.project.githubRepo }, summary: { totalPages: allPages.length, built: builtCount, inProgress: inProgressCount, missing: missingCount, completionPercentage: Math.round((builtCount / allPages.length) * 100) }, visionSummary: extractVisionPillars(vision), mvpChecklist: corePages, nextSteps: generateNextSteps(corePages, missingFeatures), generatedAt: new Date().toISOString(), // Empty trees for fallback (will be populated when AI generation works) journeyTree: { label: "Journey", nodes: [] }, touchpointsTree: { label: "Touchpoints", nodes: [] }, systemTree: { label: "System", nodes: [] }, }; } function detectPageStatus(pagePath: string, commitMessages: string[], builtFeatures: any[]): string { const searchTerms = pagePath.split('|'); for (const term of searchTerms) { const hasCommit = commitMessages.some(msg => msg.toLowerCase().includes(term.toLowerCase()) ); const hasFeature = builtFeatures.some(f => f.name.toLowerCase().includes(term.toLowerCase()) || f.evidence.some((e: string) => e.toLowerCase().includes(term.toLowerCase())) ); if (hasCommit || hasFeature) { return 'built'; } } return 'missing'; } function findEvidence(searchTerm: string, commitMessages: string[]): string[] { const terms = searchTerm.split('|'); const evidence: string[] = []; for (const term of terms) { const matches = commitMessages.filter(msg => msg.toLowerCase().includes(term.toLowerCase()) ); evidence.push(...matches.slice(0, 2)); } return evidence; } function extractVisionPillars(vision: string): string[] { const pillars = []; if (vision.includes('start from scratch') || vision.includes('import')) { pillars.push('Project ingestion (start from scratch or import existing work)'); } if (vision.includes('understand') || vision.includes('vision')) { pillars.push('Project understanding (vision, history, structure, metadata)'); } if (vision.includes('plan') || vision.includes('checklist')) { pillars.push('Project planning (auto-generated v1 roadmap/checklist)'); } if (vision.includes('marketing') || vision.includes('communication') || vision.includes('automation')) { pillars.push('Automation + AI support (marketing, chat, context-aware support)'); } return pillars; } function generateNextSteps(corePages: any[], missingFeatures: any[]): any[] { const steps = []; // Find critical missing pages const criticalMissing = corePages .flatMap(c => c.pages) .filter(p => p.status === 'missing' && p.priority === 'critical'); for (const page of criticalMissing.slice(0, 3)) { steps.push({ priority: 1, task: `Build ${page.title}`, path: page.path || '', reason: page.note || 'Critical for MVP launch' }); } // Add missing features if (missingFeatures && Array.isArray(missingFeatures)) { for (const feature of missingFeatures.slice(0, 2)) { if (feature && (feature.feature || feature.task)) { steps.push({ priority: 2, task: feature.feature || feature.task || 'Complete missing feature', reason: feature.reason || 'Important for MVP' }); } } } return steps; } /** * Prepare vision input from project data * Maps project vision to the 3-question format */ function prepareVisionInput(projectData: any, history: any) { const vision = projectData.vision || history.project?.vision || ''; // Try to extract answers from vision field // If vision is structured with questions, parse them // Otherwise, treat entire vision as the story (q2) return { q1_who_and_problem: { prompt: "Who has the problem you want to fix and what is it?", raw_answer: projectData.visionAnswers?.q1 || extractProblemFromVision(vision) || vision }, q2_story: { prompt: "Tell me a story of this person using your tool and experiencing your vision?", raw_answer: projectData.visionAnswers?.q2 || vision }, q3_improvement: { prompt: "How much did that improve things for them?", raw_answer: projectData.visionAnswers?.q3 || extractImprovementFromVision(vision) || 'Significantly faster and more efficient workflow' } }; } /** * Extract problem statement from unstructured vision */ function extractProblemFromVision(vision: string): string { // Simple heuristic: Look for problem-related keywords const problemKeywords = ['problem', 'struggle', 'difficult', 'challenge', 'pain', 'need']; const sentences = vision.split(/[.!?]+/); for (const sentence of sentences) { const lowerSentence = sentence.toLowerCase(); if (problemKeywords.some(keyword => lowerSentence.includes(keyword))) { return sentence.trim(); } } return vision.split(/[.!?]+/)[0]?.trim() || vision; } /** * Extract improvement/value from unstructured vision */ function extractImprovementFromVision(vision: string): string { // Look for value/benefit keywords const valueKeywords = ['faster', 'better', 'easier', 'save', 'improve', 'automate', 'help']; const sentences = vision.split(/[.!?]+/); for (const sentence of sentences) { const lowerSentence = sentence.toLowerCase(); if (valueKeywords.some(keyword => lowerSentence.includes(keyword))) { return sentence.trim(); } } return ''; } /** * Transform AI response trees into our existing checklist format */ function transformAIResponseToChecklist(aiResponse: any, history: any, analysis: any) { const { journey_tree, touchpoints_tree, system_tree, summary } = aiResponse; // Scan commit messages for evidence const commitMessages = history.chronologicalEvents ?.filter((e: any) => e.type === 'git_commit') ?.map((e: any) => e.data.message) || []; const builtFeatures = analysis.codebaseAnalysis?.builtFeatures || []; // Combine touchpoints and system into categories const categories: any[] = []; // Process Touchpoints tree if (touchpoints_tree?.nodes) { const touchpointCategories = groupAssetsByCategory( touchpoints_tree.nodes, 'touchpoint', commitMessages, builtFeatures ); categories.push(...touchpointCategories); } // Process System tree if (system_tree?.nodes) { const systemCategories = groupAssetsByCategory( system_tree.nodes, 'system', commitMessages, builtFeatures ); categories.push(...systemCategories); } // Calculate statistics const allPages = categories.flatMap(c => c.pages); const builtCount = allPages.filter((p: any) => p.status === 'built').length; const inProgressCount = allPages.filter((p: any) => p.status === 'in_progress').length; const missingCount = allPages.filter((p: any) => p.status === 'missing').length; return { project: { name: history.project.name, vision: history.project.vision, githubRepo: history.project.githubRepo }, summary: { totalPages: allPages.length, built: builtCount, inProgress: inProgressCount, missing: missingCount, completionPercentage: Math.round((builtCount / allPages.length) * 100) }, visionSummary: [summary || 'AI-generated MVP plan'], mvpChecklist: categories, nextSteps: generateNextStepsFromAI(allPages), generatedAt: new Date().toISOString(), aiGenerated: true, // Include raw trees for Journey/Design/Tech views journeyTree: journey_tree, touchpointsTree: touchpoints_tree, systemTree: system_tree, }; } /** * Group asset nodes by category */ function groupAssetsByCategory( nodes: any[], listType: 'touchpoint' | 'system', commitMessages: string[], builtFeatures: any[] ) { const categoryMap = new Map(); for (const node of nodes) { const category = inferCategory(node, listType); if (!categoryMap.has(category)) { categoryMap.set(category, []); } const page = { id: node.id, path: inferPath(node), title: node.name, status: detectAINodeStatus(node, commitMessages, builtFeatures), priority: node.must_have_for_v1 ? 'critical' : 'medium', evidence: findEvidenceForNode(node, commitMessages), note: node.asset_metadata?.why_it_exists, metadata: node.asset_metadata, requirements: flattenChildrenToRequirements(node.children) }; categoryMap.get(category)!.push(page); } return Array.from(categoryMap.entries()).map(([category, pages]) => ({ category, pages })); } /** * Infer category from node metadata */ function inferCategory(node: any, listType: 'touchpoint' | 'system'): string { const assetType = node.asset_type; const journeyStage = node.asset_metadata?.journey_stage || ''; if (listType === 'system') { if (assetType === 'api_endpoint' || assetType === 'service') return 'Core Features'; if (assetType === 'integration') return 'Settings'; return 'Settings'; } // Touchpoints if (assetType === 'flow') return 'Flows'; if (assetType === 'social_post') return 'Social'; if (assetType === 'document') return 'Content'; if (assetType === 'email') return 'Marketing'; if (journeyStage.toLowerCase().includes('aware') || journeyStage.toLowerCase().includes('discover')) { return 'Marketing'; } return 'Core Features'; } /** * Infer path from node */ function inferPath(node: any): string { // Try to extract path from implementation_notes or name const implNotes = node.asset_metadata?.implementation_notes || ''; const pathMatch = implNotes.match(/\/[\w\-\/\[\]]+/); if (pathMatch) return pathMatch[0]; // Generate a reasonable path from name and type const slug = node.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9\-]/g, ''); if (node.asset_type === 'api_endpoint') return `/api/${slug}`; if (node.asset_type === 'flow') return `flow/${slug}`; return `/${slug}`; } /** * Detect status of AI node based on existing work */ function detectAINodeStatus(node: any, commitMessages: string[], builtFeatures: any[]): string { const name = node.name.toLowerCase(); const path = inferPath(node).toLowerCase(); // Check commit messages const hasCommit = commitMessages.some(msg => msg.toLowerCase().includes(name) || msg.toLowerCase().includes(path) ); // Check built features const hasFeature = builtFeatures.some((f: any) => f.name?.toLowerCase().includes(name) || f.evidence?.some((e: string) => e.toLowerCase().includes(name)) ); if (hasCommit || hasFeature) return 'built'; return node.must_have_for_v1 ? 'missing' : 'missing'; } /** * Find evidence for a node in commit messages */ function findEvidenceForNode(node: any, commitMessages: string[]): string[] { const name = node.name.toLowerCase(); const evidence = commitMessages .filter(msg => msg.toLowerCase().includes(name)) .slice(0, 2); return evidence; } /** * Flatten children nodes to requirements */ function flattenChildrenToRequirements(children: any[]): any[] { if (!children || children.length === 0) return []; return children.map((child, index) => ({ id: index + 1, text: child.name, status: 'missing' })); } /** * Generate next steps from AI-generated pages */ function generateNextStepsFromAI(pages: any[]): any[] { const criticalMissing = pages .filter((p: any) => p.status === 'missing' && p.priority === 'critical') .slice(0, 5); return criticalMissing.map((page: any, index: number) => ({ priority: index + 1, task: `Build ${page.title}`, path: page.path || '', reason: page.note || 'Critical for MVP V1' })); }