import { listChatExtractions } from '@/lib/server/chat-extraction'; import { clamp, nowIso, persistPhaseArtifacts, uniqueStrings, toStage } from '@/lib/server/projects'; import type { CanonicalProductModel } from '@/lib/types/product-model'; import type { ChatExtractionRecord } from '@/lib/types/chat-extraction'; const average = (numbers: number[]) => numbers.length ? numbers.reduce((sum, value) => sum + value, 0) / numbers.length : 0; export async function buildCanonicalProductModel(projectId: string): Promise { const extractions = await listChatExtractions(projectId); if (!extractions.length) { throw new Error('No chat extractions found for project'); } const completionAvg = average( extractions.map( (record) => (record.data as any)?.summary_scores?.overall_completion ?? record.overallCompletion ?? 0, ), ); const confidenceAvg = average( extractions.map( (record) => (record.data as any)?.summary_scores?.overall_confidence ?? record.overallConfidence ?? 0, ), ); const canonical = mapExtractionToCanonical( projectId, pickHighestConfidence(extractions as any), completionAvg, confidenceAvg, ); await persistPhaseArtifacts(projectId, (phaseData, phaseScores, phaseHistory) => { phaseData.canonicalProductModel = canonical; phaseScores.vision = { overallCompletion: canonical.overallCompletion, overallConfidence: canonical.overallConfidence, updatedAt: nowIso(), }; phaseHistory.push({ phase: 'vision', status: 'completed', timestamp: nowIso() }); return { phaseData, phaseScores, phaseHistory, nextPhase: 'vision_ready' }; }); return canonical; } function pickHighestConfidence(records: ChatExtractionRecord[]) { return records.reduce((best, record) => record.overallConfidence > best.overallConfidence ? record : best, ); } function mapExtractionToCanonical( projectId: string, record: ChatExtractionRecord, completionAvg: number, confidenceAvg: number, ): CanonicalProductModel { const data = record.data; const coreFeatures = data.solution_and_features.core_features.map( (feature) => feature.name || feature.description, ); const niceToHaveFeatures = data.solution_and_features.nice_to_have_features.map( (feature) => feature.name || feature.description, ); return { projectId, workingTitle: data.project_summary.working_title ?? null, oneLiner: data.project_summary.one_liner ?? null, problem: data.product_vision.problem_statement.description ?? null, targetUser: data.target_users.primary_segment.description ?? null, desiredOutcome: data.product_vision.target_outcome.description ?? null, coreSolution: data.solution_and_features.core_solution.description ?? null, coreFeatures: uniqueStrings(coreFeatures), niceToHaveFeatures: uniqueStrings(niceToHaveFeatures), marketCategory: data.market_and_competition.market_category.description ?? null, competitors: uniqueStrings( data.market_and_competition.competitors.map((competitor) => competitor.name), ), techStack: uniqueStrings( data.tech_and_constraints.stack_mentions.map((item) => item.description), ), constraints: uniqueStrings( data.tech_and_constraints.constraints.map((constraint) => constraint.description), ), currentStage: toStage(data.project_summary.stage), shortTermGoals: uniqueStrings( data.goals_and_success.short_term_goals.map((goal) => goal.description), ), longTermGoals: uniqueStrings( data.goals_and_success.long_term_goals.map((goal) => goal.description), ), overallCompletion: clamp(completionAvg), overallConfidence: clamp(confidenceAvg), }; }