967 lines
33 KiB
TypeScript
967 lines
33 KiB
TypeScript
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<string, any[]>();
|
|
|
|
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'
|
|
}));
|
|
}
|
|
|