import { callAI } from '@/services/ai' import { getPrompt } from '@/services/prompts-db' import { getSessionsByProject, getSessionSummary, getProjectState } from '@/services/transcriptions-db' import { saveDraft, createDraftId } from '@/services/hu-drafts-db' import { analyzeExisting, assignEpicCodes, assignItemCodes } from '@/services/hierarchy-generator' import type { EnrichedEpic, EnrichedUserStory } from '@/stores/workitems' export interface AnalysisHU { title: string description: string acceptance_criteria: string[] priority: string story_points?: number type?: string feature?: string sprint?: number epicName?: string // nombre de la épica a la que pertenece } export interface AnalysisEpic { name: string description: string linkedHuTitles: string[] estimatedStart?: string estimatedEnd?: string } interface AnalysisEpicsResult { epics: AnalysisEpic[] summary: string rationale: string // explicación de por qué estas épicas } interface AnalysisHUsResult { hus: AnalysisHU[] summary: string } async function buildProjectContext(projectId: number, projectName: string, existingHUs: EnrichedUserStory[]) { const sessions = await getSessionsByProject(projectId) const state = await getProjectState(projectId) const sessionsWithSummaries = [] for (const s of sessions) { const summary = await getSessionSummary(s.id!) sessionsWithSummaries.push({ date: s.date, title: s.title, file: s.fileName, summary: summary?.summary?.slice(0, 500) || '', tasks: (safeParse<{ description: string; priority: string }[]>(summary?.tasks, [])).map(t => ({ d: t.description, p: t.priority })), decisions: safeParse(summary?.decisions, []), }) } return { projectName, existingHUs: existingHUs.map(h => ({ t: h._cleanTitle || h.title, s: h.status, p: h.priority, e: h._assignedName || '' })), sessions: sessionsWithSummaries.slice(-10).reverse(), projectState: state ? { summary: state.summary?.slice(0, 500), objectives: safeParse(state.objectives, []), tasks: (safeParse<{ status: string }[]>(state.tasks, [])).filter(t => t.status !== 'completada').slice(0, 30), } : null, } } // ─── FASE 1: Generar solo Épicas ────────────────────── export async function analyzeProjectEpics( projectId: number, projectName: string, existingHUs: EnrichedUserStory[], existingEpics: string[], // nombres de épicas que ya están en KAPPA signal?: AbortSignal, existingEpicItems?: EnrichedEpic[], // épicas enriquecidas para contar códigos ): Promise { const context = await buildProjectContext(projectId, projectName, existingHUs) const userContent = `Contexto del proyecto:\n${JSON.stringify(context, null, 0)}\n\nÉpicas ya existentes en KAPPA: ${existingEpics.join(', ') || 'Ninguna'}` console.log(`[Alpha] Phase 1 — Analyzing epics for ${projectName}, ${existingEpics.length} existentes`) const systemPrompt = await getPrompt('project_gap') // Pasamos una instrucción adicional para que solo genere épicas en esta fase const phasePrompt = `[FASE 1: SOLO ÉPICAS] Analizá TODO el contexto del proyecto. Identificá las épicas necesarias. Una épica agrupa funcionalidades de un mismo tema (ej: "Módulo de Pagos", "Scrapers", "Dashboard"). Revisá si las épicas ya existentes en KAPPA cubren las necesidades. Si una épica ya existe en KAPPA, NO la generes de nuevo. Si el acta de inicio, sesiones o documentación mencionan funcionalidades que no están cubiertas por ninguna épica existente, proponé nuevas épicas. Para cada épica, listá los títulos tentativos de las HUs que pertenecerían a ella (linkedHuTitles). ${systemPrompt} IMPORTANTE: Respondé SOLO con épicas en esta fase. NO generes HUs aún.` const content = await callAI( [{ role: 'system', content: phasePrompt }, { role: 'user', content: userContent }], 0.3, 8192, signal, ) try { const jsonStr = extractJSON(content) const parsed = JSON.parse(jsonStr) const rawEpics: AnalysisEpic[] = parsed.epics || parsed.epic || [] // Asignar códigos jerárquicos (E06, E07...) a las épicas propuestas const counters = analyzeExisting(existingEpicItems || [], existingHUs) const epicsWithCodes = assignEpicCodes(rawEpics, counters) const result: AnalysisEpicsResult = { epics: epicsWithCodes, summary: parsed.summary || '', rationale: parsed.rationale || parsed.summary || '', } console.log(`[Alpha] Phase 1 complete: ${result.epics.length} épicas propuestas`) return result } catch (e) { console.error('[Alpha] Failed to parse epics analysis. Raw:', content) throw new Error('No se pudieron generar las épicas') } } // ─── FASE 2: Generar HUs dentro de las épicas confirmadas ── export async function analyzeProjectHUs( projectId: number, projectName: string, existingHUs: EnrichedUserStory[], confirmedEpics: AnalysisEpic[], // épicas que el usuario aceptó y ya están o serán enviadas a KAPPA signal?: AbortSignal, ): Promise { const context = await buildProjectContext(projectId, projectName, existingHUs) const epicsDetail = confirmedEpics.map(e => `- ${e.name}: ${e.description?.slice(0, 200)} (HUs sugeridas: ${e.linkedHuTitles.join(', ')})` ).join('\n') const userContent = `Contexto del proyecto:\n${JSON.stringify(context, null, 0)}\n\nÉpicas confirmadas:\n${epicsDetail}` console.log(`[Alpha] Phase 2 — Generating HUs for ${confirmedEpics.length} epics in ${projectName}`) const systemPrompt = await getPrompt('project_gap') const phasePrompt = `[FASE 2: GENERAR HUs] Las siguientes épicas ya están definidas. Tu tarea es generar las HUs (feature, task, US, bug, spike, etc.) que pertenecen a cada épica. Cada HU debe estar vinculada a UNA épica existente. Usá el campo "epicName" para indicar a qué épica pertenece cada HU. No generes HUs duplicadas con las que ya existen en KAPPA. Incluí para cada HU: título, descripción, criterios de aceptación, prioridad, story points, tipo, feature, sprint estimado. ${systemPrompt} IMPORTANTE: Respondé SOLO con HUs en esta fase. NO generes épicas nuevas.` const content = await callAI( [{ role: 'system', content: phasePrompt }, { role: 'user', content: userContent }], 0.3, 8192, signal, ) try { const jsonStr = extractJSON(content) const parsed = JSON.parse(jsonStr) const rawHUs: AnalysisHU[] = parsed.hus || [] // Asignar códigos jerárquicos (E01-F04, E01-T01...) a las HUs propuestas const counters = analyzeExisting([], existingHUs) const epicsWithCodes = confirmedEpics.map(e => ({ name: e.name, epicCode: (e as any).epicCode || '', })) const husWithCodes = assignItemCodes(rawHUs, epicsWithCodes, counters) const result: AnalysisHUsResult = { hus: husWithCodes, summary: parsed.summary || '', } console.log(`[Alpha] Phase 2 complete: ${result.hus.length} HUs generadas con códigos`) return result } catch (e) { console.error('[Alpha] Failed to parse HUs analysis. Raw:', content) throw new Error('No se pudieron generar las HUs') } } // ─── Guardar drafts ─────────────────────────────────── export async function saveEpicDrafts( projectId: number, epics: AnalysisEpic[], existingHUs: EnrichedUserStory[], ): Promise<{ saved: number; skipped: number }> { let saved = 0 let skipped = 0 for (const epic of epics) { const epicWithCode = epic as any const epicCode = epicWithCode.epicCode || '' const fullTitle = epicCode ? `[${epicCode}] ${epic.name}` : epic.name const normalizedName = epic.name.toLowerCase().trim() const isDuplicate = existingHUs.some(ex => (ex._cleanTitle || ex.title).toLowerCase().trim() === normalizedName) if (isDuplicate) { skipped++; continue } await saveDraft({ id: createDraftId(), projectId, title: fullTitle, description: epic.description, acceptanceCriteria: '', priority: 'Media', type: 'E', metadata: JSON.stringify({ linkedHuTitles: epic.linkedHuTitles, estimatedStart: epic.estimatedStart, estimatedEnd: epic.estimatedEnd, epicCode, }), syncStatus: 'draft', createdAt: new Date().toISOString(), }) saved++ } return { saved, skipped } } export async function saveHUDrafts( projectId: number, hus: AnalysisHU[], existingHUs: EnrichedUserStory[], ): Promise<{ saved: number; skipped: number }> { let saved = 0 let skipped = 0 for (const hu of hus) { const normalizedTitle = hu.title.toLowerCase().trim() const isDuplicate = existingHUs.some(ex => { const et = (ex._cleanTitle || ex.title).toLowerCase().trim() return et === normalizedTitle || et.includes(normalizedTitle) || normalizedTitle.includes(et) }) if (isDuplicate) { skipped++; continue } const draftId = createDraftId() await saveDraft({ id: draftId, projectId, title: hu.title, description: hu.description, acceptanceCriteria: hu.acceptance_criteria.join('\n'), priority: hu.priority, type: hu.type === 'task' ? 'T' : hu.type === 'bug' ? 'B' : hu.type === 'feature' ? 'F' : 'U', metadata: JSON.stringify({ epicName: hu.epicName, feature: hu.feature, sprint: hu.sprint, storyPoints: hu.story_points, }), syncStatus: 'draft', createdAt: new Date().toISOString(), }) saved++ } return { saved, skipped } } function extractJSON(text: string): string { try { JSON.parse(text); return text } catch {} const block = text.match(/```(?:json)?\s*([\s\S]*?)```/) if (block) { try { JSON.parse(block[1].trim()); return block[1].trim() } catch {} } const first = text.indexOf('{') const last = text.lastIndexOf('}') if (first !== -1 && last > first) { const c = text.slice(first, last + 1); try { JSON.parse(c); return c } catch {} } return text } function safeParse(json: string | undefined | null, fallback: T): T { if (!json) return fallback try { return JSON.parse(json) as T } catch { return fallback } }