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:
+2
-1
@@ -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
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user