From 13683ec2c410d15f7f357097400ccc9a32eeaa9b Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Thu, 28 May 2026 14:38:39 -0500 Subject: [PATCH] =?UTF-8?q?project-analyzer:=20propone=20=C3=A9picas=20+?= =?UTF-8?q?=20vincula=20HUs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/services/db.ts | 3 +- src/services/project-analyzer.ts | 97 +++++++++++++++------------- src/views/DashboardView.vue | 104 +++++++++++++++++++++---------- 3 files changed, 127 insertions(+), 77 deletions(-) diff --git a/src/services/db.ts b/src/services/db.ts index d8a76ff..e3828a7 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -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 diff --git a/src/services/project-analyzer.ts b/src/services/project-analyzer.ts index ac32145..c8b7b68 100644 --- a/src/services/project-analyzer.ts +++ b/src/services/project-analyzer.ts @@ -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 { - // 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++ } diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index eaee303..6b49063 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -32,6 +32,12 @@ const emit = defineEmits<{ }>() // ─── Drafts ────────────────────────────────────────────── +function getEpicLinkedHUs(d: HuDraftRecord): string[] { + try { + const meta = JSON.parse(d.metadata || '{}') + return meta.linkedHuTitles || [] + } catch { return [] } +} const drafts = ref([]) const pushingDraftId = ref(null) @@ -46,36 +52,59 @@ async function pushDraft(d: HuDraftRecord) { await dbSaveDraft(d) try { const token = localStorage.getItem('kappa_token') - const res = await fetch('/api/userstorys/create/', { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }, - body: JSON.stringify({ - initiative: String(d.projectId), - title: d.title, - description: d.description ? `

${d.description.replace(/\n/g, '

')}

` : '', - criterios_aceptacion: d.acceptanceCriteria ? `

${d.acceptanceCriteria.replace(/\n/g, '

')}

` : '', - story_points: '', - priority: d.priority === 'Alta' ? '1' : d.priority === 'Baja' ? '3' : '2', - sprint: '', - asignado_a: [], - client_taker: null, - characterization_hu: '', - has_impairment: false, - epic_development: null, - feature: '', - initial_date: null, - end_date: null, - }), - }) - if (res.ok) { - await deleteDraft(d.id) + const headers = { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) } + + if (d.type === 'E') { + // Push épica: buscar HUs vinculadas ya enviadas + const meta = JSON.parse(d.metadata || '{}') + const linkedHuIds: number[] = [] + for (const huTitle of meta.linkedHuTitles || []) { + const pushedHu = drafts.value.find(x => x.type !== 'E' && (x.title.toLowerCase().trim() === huTitle.toLowerCase().trim()) && x.kappaId) + if (pushedHu?.kappaId) linkedHuIds.push(pushedHu.kappaId) + } + + const res = await fetch('/api/epicdevelopment/create/', { + method: 'POST', headers, + body: JSON.stringify({ + initiative: String(d.projectId), name: d.title, + description: d.description ? `

${d.description.replace(/\n/g, '

')}

` : '', + client_taker: null, hu: linkedHuIds, + stimated_start_date: meta.estimatedStart || null, + stimated_end_date: meta.estimatedEnd || null, + }), + }) + if (res.ok) { + d.syncStatus = 'pushed'; await dbSaveDraft(d) + } else { + d.syncStatus = 'draft'; await dbSaveDraft(d) + } } else { - d.syncStatus = 'draft' - await dbSaveDraft(d) + // Push HU individual + const res = await fetch('/api/userstorys/create/', { + method: 'POST', headers, + body: JSON.stringify({ + initiative: String(d.projectId), title: d.title, + description: d.description ? `

${d.description.replace(/\n/g, '

')}

` : '', + criterios_aceptacion: d.acceptanceCriteria ? `

${d.acceptanceCriteria.replace(/\n/g, '

')}

` : '', + story_points: '', + priority: d.priority === 'Alta' ? '1' : d.priority === 'Baja' ? '3' : '2', + sprint: '', asignado_a: [], client_taker: null, + characterization_hu: '', has_impairment: false, + epic_development: null, feature: '', + initial_date: null, end_date: null, + }), + }) + if (res.ok) { + const created = await res.json() + d.kappaId = created.id || undefined + d.syncStatus = 'pushed' + await dbSaveDraft(d) + } else { + d.syncStatus = 'draft'; await dbSaveDraft(d) + } } } catch { - d.syncStatus = 'draft' - await dbSaveDraft(d) + d.syncStatus = 'draft'; await dbSaveDraft(d) } finally { pushingDraftId.value = null await loadDrafts() @@ -383,17 +412,28 @@ const statusLabel = (status: unknown) => {
-

{{ d.title }}

-

{{ d.description }}

+
+ + {{ d.type === 'E' ? 'Épica' : 'HU' }} + +

{{ d.title }}

+
+
+ + HUs vinculadas: {{ getEpicLinkedHUs(d).join(', ') }} + + Sin HUs vinculadas +
+

{{ d.description }}

{{ d.priority }} - - {{ d.syncStatus === 'pushing' ? 'Enviando...' : d.syncStatus === 'pushed' ? 'Enviada' : 'Borrador' }} + + {{ d.syncStatus === 'pushing' ? 'Enviando...' : d.syncStatus === 'pushed' ? `Enviada (ID ${d.kappaId || '?'})` : 'Borrador' }}
-