VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

View File

@@ -0,0 +1,966 @@
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'
}));
}