Análisis IA en dos fases: épicas primero, luego HUs vinculadas con epic_development

- project-analyzer.ts: dividido en analyzeProjectEpics() y analyzeProjectHUs()
  Fase 1: genera solo épicas con linkedHuTitles
  Fase 2: genera HUs dentro de épicas con epicName
- saveEpicDrafts / saveHUDrafts separados para cada tipo
- DashboardView: dos botones '1. Generar Épicas' y '2. Generar HUs'
- pushDraft épica: al crear en KAPPA, actualiza metadata de HUs vinculadas con epicDevelopment
- pushDraft HU: envía epic_development + payload completo (feature, sprint, asignado_a, etc.)
- project_gap prompt: instrucciones separadas para FASE 1 (épicas) y FASE 2 (HUs)
This commit is contained in:
2026-05-29 17:33:47 -05:00
parent a4245017f8
commit 9cf12b482f
4 changed files with 353 additions and 101 deletions
+146 -44
View File
@@ -2,7 +2,6 @@ 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 { generateAndSavePlan } from '@/services/qa-analyzer'
import type { EnrichedUserStory } from '@/stores/workitems'
export interface AnalysisHU {
@@ -10,6 +9,11 @@ export interface AnalysisHU {
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 {
@@ -20,18 +24,18 @@ export interface AnalysisEpic {
estimatedEnd?: string
}
interface AnalysisResult {
hus: AnalysisHU[]
interface AnalysisEpicsResult {
epics: AnalysisEpic[]
summary: string
rationale: string // explicación de por qué estas épicas
}
interface AnalysisHUsResult {
hus: AnalysisHU[]
summary: string
}
export async function analyzeProject(
projectId: number,
projectName: string,
existingHUs: EnrichedUserStory[],
signal?: AbortSignal,
): Promise<AnalysisResult> {
async function buildProjectContext(projectId: number, projectName: string, existingHUs: EnrichedUserStory[]) {
const sessions = await getSessionsByProject(projectId)
const state = await getProjectState(projectId)
@@ -48,9 +52,9 @@ export async function analyzeProject(
})
}
const context = {
return {
projectName,
existingHUs: existingHUs.map(h => ({ t: h._cleanTitle || h.title, s: h.status, p: h.priority })),
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),
@@ -58,39 +62,150 @@ export async function analyzeProject(
tasks: (safeParse<{ status: string }[]>(state.tasks, [])).filter(t => t.status !== 'completada').slice(0, 30),
} : null,
}
}
const userContent = `Contexto completo del proyecto en JSON:\n${JSON.stringify(context, null, 0)}`
// ─── FASE 1: Generar solo Épicas ──────────────────────
console.log(`[Alpha] Project analysis — ${projectId}, ${existingHUs.length} HUs existentes, ${sessions.length} sesiones`)
export async function analyzeProjectEpics(
projectId: number,
projectName: string,
existingHUs: EnrichedUserStory[],
existingEpics: string[], // épicas que ya están en KAPPA
signal?: AbortSignal,
): Promise<AnalysisEpicsResult> {
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: systemPrompt }, { role: 'user', content: userContent }],
[{ role: 'system', content: phasePrompt }, { role: 'user', content: userContent }],
0.3, 8192, signal,
)
try {
const jsonStr = extractJSON(content)
const result: AnalysisResult = JSON.parse(jsonStr)
console.log(`[Alpha] Analysis result: ${result.hus.length} HUs, ${result.epics?.length || 0} épicas`)
const parsed = JSON.parse(jsonStr)
const result: AnalysisEpicsResult = {
epics: parsed.epics || parsed.epic || [],
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 analysis. Raw:', content)
throw new Error('No se pudo procesar el análisis del proyecto')
console.error('[Alpha] Failed to parse epics analysis. Raw:', content)
throw new Error('No se pudieron generar las épicas')
}
}
export async function saveAsDrafts(
// ─── FASE 2: Generar HUs dentro de las épicas confirmadas ──
export async function analyzeProjectHUs(
projectId: number,
analysis: AnalysisResult,
projectName: string,
existingHUs: EnrichedUserStory[],
confirmedEpics: AnalysisEpic[], // épicas que el usuario aceptó y ya están o serán enviadas a KAPPA
signal?: AbortSignal,
): Promise<AnalysisHUsResult> {
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 result: AnalysisHUsResult = {
hus: parsed.hus || [],
summary: parsed.summary || '',
}
console.log(`[Alpha] Phase 2 complete: ${result.hus.length} HUs generadas`)
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[],
sourceSessionId?: number,
): Promise<{ saved: number; skipped: number }> {
let saved = 0
let skipped = 0
// Guardar HUs
for (const hu of analysis.hus || []) {
for (const epic of epics) {
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: epic.name,
description: epic.description, acceptanceCriteria: '',
priority: 'Media', type: 'E',
metadata: JSON.stringify({
linkedHuTitles: epic.linkedHuTitles,
estimatedStart: epic.estimatedStart,
estimatedEnd: epic.estimatedEnd,
}),
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()
@@ -102,27 +217,14 @@ export async function saveAsDrafts(
await saveDraft({
id: draftId, projectId, title: hu.title,
description: hu.description, acceptanceCriteria: hu.acceptance_criteria.join('\n'),
priority: hu.priority, type: 'U', metadata: '{}',
sourceSessionId, syncStatus: 'draft', createdAt: new Date().toISOString(),
})
// QA plan: fire-and-forget para no bloquear el guardado
generateAndSavePlan(projectId, draftId, hu.title, hu.description, hu.acceptance_criteria.join('\n'))
.catch(e => console.error(`[Alpha] QA auto-gen failed for ${hu.title}:`, e))
saved++
}
// Guardar épicas
for (const epic of analysis.epics || []) {
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: epic.name,
description: epic.description, acceptanceCriteria: '',
priority: 'Media', type: 'E',
metadata: JSON.stringify({ linkedHuTitles: epic.linkedHuTitles, estimatedStart: epic.estimatedStart, estimatedEnd: epic.estimatedEnd }),
sourceSessionId, syncStatus: 'draft', createdAt: new Date().toISOString(),
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++
}
+39 -12
View File
@@ -40,38 +40,65 @@ Responde SOLO con JSON válido en este formato:
},
project_gap: {
label: 'Análisis de brechas del proyecto',
content: `Eres un analista funcional experto. Tu tarea es analizar TODO el contexto de un proyecto y generar las Épicas e Historias de Usuario (HUs) que faltan.
content: `Eres un analista funcional experto en metodologías ágiles.
Trabajás en DOS FASES separadas. Cada fase tiene sus propias instrucciones.
Reglas:
Reglas generales:
1. Analizá TODA la información disponible: sesiones, resúmenes, estado del proyecto, HUs existentes
2. Identificá requisitos, funcionalidades, mejoras o bugs que NO estén cubiertos
3. Agrupá HUs relacionadas en Épicas. Cada épica agrupa funcionalidades de un mismo tema
4. Cada HU debe tener: título claro, descripción, criterios de aceptación verificables
5. No generes duplicados. Compará con la lista existente
6. Priorizá según urgencia implícita (Alta/Media/Baja)
7. Si todo ya está cubierto, devolvé arreglos vacíos
8. Respondé SOLO con JSON válido
3. No generes duplicados. Compará con TODO lo que ya existe
4. Respondé SOLO con JSON válido
5. Si todo ya está cubierto, devolvé arreglos vacíos
Formato de respuesta:
--- FASE 1: Generar Épicas ---
Instrucciones específicas:
- Identificá las épicas necesarias agrupando funcionalidades por tema
- Revisá si las épicas ya existentes cubren las necesidades
- Si una épica ya existe en KAPPA, NO la generes de nuevo
- Para cada épica, listá los títulos tentativos de las HUs que pertenecerían a ella (linkedHuTitles)
- Incluí un campo "rationale" explicando por qué proponés cada épica
- NO generes HUs en esta fase
Formato de respuesta FASE 1:
{
"epics": [
{
"name": "Nombre de la Épica",
"name": "Nombre de la Épica (ej: Módulo de Pagos)",
"description": "Descripción de la épica",
"linkedHuTitles": ["Título HU 1", "Título HU 2"],
"estimatedStart": "YYYY-MM-DD",
"estimatedEnd": "YYYY-MM-DD"
}
],
"summary": "Resumen del análisis de épicas",
"rationale": "Explicación de por qué estas épicas y no otras"
}
--- FASE 2: Generar HUs dentro de Épicas ---
Instrucciones específicas:
- Las épicas ya están definidas. Generá las HUs que pertenecen a cada una.
- Cada HU debe tener un campo "epicName" indicando a qué épica pertenece
- Tipos de HU: feature, task, US (historia de usuario), bug, spike
- Incluí: título, descripción, criterios de aceptación, prioridad, story points, tipo, feature, sprint estimado
- No generes HUs duplicadas con las existentes
- NO generes épicas en esta fase
Formato de respuesta FASE 2:
{
"hus": [
{
"title": "Título de la HU",
"description": "Descripción detallada",
"acceptance_criteria": ["Criterio 1", "Criterio 2"],
"priority": "Alta|Media|Baja"
"priority": "Alta|Media|Baja",
"story_points": 3,
"type": "feature|task|bug|spike",
"feature": "Nombre de la feature",
"sprint": 12,
"epicName": "Nombre de la épica a la que pertenece"
}
],
"summary": "Resumen del análisis"
"summary": "Resumen de las HUs generadas"
}`,
},
session: {