project-analyzer: propone épicas + vincula HUs

- AnalysisEpic: name, description, linkedHuTitles, estimated dates
- saveAsDrafts: guarda épicas como drafts tipo 'E' con metadata JSON
- HuDraftRecord: +metadata field (JSON string)
- DashboardView: push épica busca HUs vinculadas con kappaId
- DashboardView: push HU guarda kappaId del response, no elimina draft
- UI: badge tipo (Epica/HU), HUs vinculadas visibles, estado Enviada con ID
This commit is contained in:
2026-05-28 14:38:39 -05:00
parent 63804a2cb6
commit 13683ec2c4
3 changed files with 127 additions and 77 deletions
+2 -1
View File
@@ -52,7 +52,8 @@ export interface HuDraftRecord {
description: string
acceptanceCriteria: string
priority: string
type: string
type: string // 'U' = HU, 'E' = Epic, 'F' = Feature, 'T' = Task, 'B' = Bug
metadata: string // JSON opcional: { linkedHuTitles?: string[], estimatedStart?: string, estimatedEnd?: string }
sourceSessionId?: number
syncStatus: 'draft' | 'pushing' | 'pushed'
kappaId?: number
+53 -44
View File
@@ -10,29 +10,43 @@ export interface AnalysisHU {
priority: string
}
export interface AnalysisEpic {
name: string
description: string
linkedHuTitles: string[]
estimatedStart?: string
estimatedEnd?: string
}
interface AnalysisResult {
hus: AnalysisHU[]
epics: AnalysisEpic[]
summary: string
}
const ANALYSIS_SYSTEM_PROMPT = `Eres un analista funcional experto. Tu tarea es analizar TODO el contexto de un proyecto y generar las Historias de Usuario (HUs) que faltan.
const ANALYSIS_SYSTEM_PROMPT = `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.
Reglas:
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 por las HUs existentes
3. Cada HU debe tener: título claro, descripción detallada, criterios de aceptación verificables
4. No generes HUs duplicadas. Compará con la lista de HUs existentes
5. Priorizá según urgencia implícita (Alta/Media/Baja)
6. Si todo ya está cubierto, devolvé un arreglo vacío
7. Respondé SOLO con JSON válido
Contexto recibido en JSON:
- existingHUs: HUs que ya existen en el proyecto (NO repetir)
- sessions: sesiones registradas con resúmenes
- projectState: estado consolidado del proyecto
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
Formato de respuesta:
{
"epics": [
{
"name": "Nombre de la Épica",
"description": "Descripción de la épica",
"linkedHuTitles": ["Título HU 1", "Título HU 2"],
"estimatedStart": "YYYY-MM-DD",
"estimatedEnd": "YYYY-MM-DD"
}
],
"hus": [
{
"title": "Título de la HU",
@@ -41,7 +55,7 @@ Formato de respuesta:
"priority": "Alta|Media|Baja"
}
],
"summary": "Resumen del análisis: cuántas HUs se crearon y por qué"
"summary": "Resumen del análisis"
}`
export async function analyzeProject(
@@ -50,11 +64,9 @@ export async function analyzeProject(
existingHUs: EnrichedUserStory[],
signal?: AbortSignal,
): Promise<AnalysisResult> {
// 1. Recolectar todo el contexto del proyecto
const sessions = await getSessionsByProject(projectId)
const state = await getProjectState(projectId)
// 2. Construir context compacto en JSON
const sessionsWithSummaries = []
for (const s of sessions) {
const summary = await getSessionSummary(s.id!)
@@ -70,11 +82,7 @@ export async function analyzeProject(
const context = {
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 })),
sessions: sessionsWithSummaries.slice(-10).reverse(),
projectState: state ? {
summary: state.summary?.slice(0, 500),
@@ -88,19 +96,14 @@ export async function analyzeProject(
console.log(`[Alpha] Project analysis — ${projectId}, ${existingHUs.length} HUs existentes, ${sessions.length} sesiones`)
const content = await callAI(
[
{ role: 'system', content: ANALYSIS_SYSTEM_PROMPT },
{ role: 'user', content: userContent },
],
0.3,
8192,
signal,
[{ role: 'system', content: ANALYSIS_SYSTEM_PROMPT }, { 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} nuevas HUs propuestas`)
console.log(`[Alpha] Analysis result: ${result.hus.length} HUs, ${result.epics?.length || 0} épicas`)
return result
} catch (e) {
console.error('[Alpha] Failed to parse analysis. Raw:', content)
@@ -108,10 +111,6 @@ export async function analyzeProject(
}
}
/**
* Guarda las HUs propuestas como borradores en la BD local.
* No crea nada en KAPPA — el usuario revisa y envía desde HuDrafts.
*/
export async function saveAsDrafts(
projectId: number,
analysis: AnalysisResult,
@@ -121,26 +120,36 @@ export async function saveAsDrafts(
let saved = 0
let skipped = 0
for (const hu of analysis.hus) {
// Guardar HUs
for (const hu of analysis.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 }
await saveDraft({
id: createDraftId(),
projectId,
title: hu.title,
description: hu.description,
acceptanceCriteria: hu.acceptance_criteria.join('\n'),
priority: hu.priority,
type: 'U',
sourceSessionId,
syncStatus: 'draft',
createdAt: new Date().toISOString(),
id: createDraftId(), 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(),
})
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(),
})
saved++
}