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:
@@ -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
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user