103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
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<CanonicalProductModel> {
|
|
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),
|
|
};
|
|
}
|
|
|
|
|