diff --git a/src/services/db.ts b/src/services/db.ts index e3828a7..09e33d1 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -60,6 +60,15 @@ export interface HuDraftRecord { createdAt: string } +export interface QAPlanRecord { + id: string + projectId: number + huId: string + huTitle: string + plan: string + createdAt: string +} + const db = new Dexie('alpha-core') as Dexie & { settings: Dexie.Table project_docs: Dexie.Table @@ -67,15 +76,17 @@ const db = new Dexie('alpha-core') as Dexie & { session_summaries: Dexie.Table project_state: Dexie.Table hu_drafts: Dexie.Table + qa_plans: Dexie.Table } -db.version(4).stores({ +db.version(5).stores({ settings: '&key', project_docs: '&projectId, projectName, updatedAt', sessions: '++id, projectId, date', session_summaries: '&sessionId', project_state: '&projectId', hu_drafts: '&id, projectId, syncStatus', + qa_plans: '&id, projectId', }) export default db diff --git a/src/services/qa-analyzer.ts b/src/services/qa-analyzer.ts new file mode 100644 index 0000000..f143925 --- /dev/null +++ b/src/services/qa-analyzer.ts @@ -0,0 +1,87 @@ +import { callAI } from '@/services/ai' +import db from '@/services/db' + +export interface QATestCase { + type: string + description: string + automatable: 'SÍ' | 'PARCIAL' | 'MANUAL' + tool: string +} + +export interface HUQAPlan { + huTitle: string + huId: string + acceptanceCriteria: string[] + testCases: QATestCase[] + manualSteps: string[] + criticalTests: string[] +} + +const QA_PROMPT = `Eres un ingeniero de QA experto. Generá un plan de pruebas detallado para una Historia de Usuario. + +Formato de respuesta JSON: +{ + "acceptanceCriteria": ["Criterio 1", "Criterio 2"], + "testCases": [ + { + "type": "Tipo de prueba", + "description": "Descripción de lo que verifica", + "automatizable": "SÍ|PARCIAL|MANUAL", + "tool": "Playwright|API Testing|Manual" + } + ], + "manualSteps": ["Paso manual 1"], + "criticalTests": ["Prueba crítica manual"] +} + +Reglas: +- SÍ = completamente automatizable +- PARCIAL = requiere validación manual complementaria +- MANUAL = requiere intervención humana +- Incluí al menos 3-5 casos de prueba +- Marcá como crítica pruebas con datos reales, ERPs externos, o cálculos financieros` + +export async function generateQAPlan(huTitle: string, huDescription: string, acceptanceCriteria: string): Promise { + const userContent = `HU: ${huTitle}\nDescripción: ${huDescription}\nCriterios: ${acceptanceCriteria}` + const content = await callAI( + [{ role: 'system', content: QA_PROMPT }, { role: 'user', content: userContent }], + 0.3, 4096, + ) + try { + const jsonStr = extractJSON(content) + const result: Omit = JSON.parse(jsonStr) + return { huTitle, huId: '', ...result } + } catch (e) { + console.error('[Alpha] Failed to parse QA plan. Raw:', content) + throw new Error(`Error generando plan QA para: ${huTitle}`) + } +} + +export async function saveQAPlan(projectId: number, huId: string, huTitle: string, plan: HUQAPlan) { + await db.table('qa_plans').put({ + id: `${projectId}-${huId}`, projectId, huId, huTitle, + plan: JSON.stringify(plan), + createdAt: new Date().toISOString(), + }) +} + +export async function getQAPlans(projectId: number) { + return db.table('qa_plans').where('projectId').equals(projectId).toArray() +} + +export async function generateAndSavePlan(projectId: number, huId: string, huTitle: string, huDescription: string, acceptanceCriteria: string): Promise { + const plan = await generateQAPlan(huTitle, huDescription, acceptanceCriteria) + plan.huId = huId + await saveQAPlan(projectId, huId, huTitle, plan) + return plan +} + +function extractJSON(text: string): string { + try { JSON.parse(text); return text } catch {} + const block = text.match(/```(?:json)?\s*([\s\S]*?)```/) + if (block) { try { JSON.parse(block[1].trim()); return block[1].trim() } catch {} } + const first = text.indexOf('{') + const last = text.lastIndexOf('}') + if (first !== -1 && last > first) { const c = text.slice(first, last + 1); try { JSON.parse(c); return c } catch {} } + return text +} diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 6b49063..a160cab 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -11,6 +11,7 @@ import AiProjectChat from '@/components/AiProjectChat.vue' import { analyzeProject, saveAsDrafts } from '@/services/project-analyzer' import { getDrafts, deleteDraft, type HuDraftRecord } from '@/services/hu-drafts-db' import { kappa } from '@/services/kappa-api' +import { generateAndSavePlan, getQAPlans, type HUQAPlan } from '@/services/qa-analyzer' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' @@ -31,6 +32,37 @@ const emit = defineEmits<{ 'navigate-settings': [] }>() +// ─── QA Plans ─────────────────────────────────────────── +const qaPlans = ref([]) +const generatingQA = ref(null) +const expandedQA = ref(null) + +async function loadQAPlans(projectId: number) { + qaPlans.value = await getQAPlans(projectId) +} + +async function generateQA(d: HuDraftRecord) { + generatingQA.value = d.id + try { + await generateAndSavePlan(d.projectId, d.id, d.title, d.description || '', d.acceptanceCriteria) + await loadQAPlans(d.projectId) + } catch (e: any) { + console.error('[Alpha] QA generation error:', e) + } finally { + generatingQA.value = null + } +} + +function parseQAPlan(record: any): HUQAPlan | null { + try { return JSON.parse(record.plan) as HUQAPlan } catch { return null } +} + +function qaBadgeColor(a: string) { + if (a === 'SÍ') return 'text-green-600 border-green-300' + if (a === 'MANUAL') return 'text-red-600 border-red-300' + return 'text-amber-600 border-amber-300' +} + // ─── Drafts ────────────────────────────────────────────── function getEpicLinkedHUs(d: HuDraftRecord): string[] { try { @@ -75,6 +107,10 @@ async function pushDraft(d: HuDraftRecord) { }) if (res.ok) { d.syncStatus = 'pushed'; await dbSaveDraft(d) + // Auto-generar QA plan tras push exitoso de épica + generateAndSavePlan(d.projectId, d.id, d.title, d.description || '', '').catch(e => + console.error(`[Alpha] QA plan auto-gen failed for epic ${d.title}:`, e) + ) } else { d.syncStatus = 'draft'; await dbSaveDraft(d) } @@ -99,6 +135,10 @@ async function pushDraft(d: HuDraftRecord) { d.kappaId = created.id || undefined d.syncStatus = 'pushed' await dbSaveDraft(d) + // Auto-generar QA plan tras push exitoso + generateAndSavePlan(d.projectId, d.id, d.title, d.description || '', d.acceptanceCriteria).catch(e => + console.error(`[Alpha] QA plan auto-gen failed for ${d.title}:`, e) + ) } else { d.syncStatus = 'draft'; await dbSaveDraft(d) } @@ -108,6 +148,7 @@ async function pushDraft(d: HuDraftRecord) { } finally { pushingDraftId.value = null await loadDrafts() + await loadQAPlans(project.value!.id) await workItems.fetchWorkItems(project.value!.id) } } @@ -117,7 +158,7 @@ async function dbSaveDraft(d: HuDraftRecord) { await saveDraft(d) } -watch(() => project.value?.id, () => { if (project.value) loadDrafts() }, { immediate: false }) +watch(() => project.value?.id, () => { if (project.value) { loadDrafts(); loadQAPlans(project.value.id) } }, { immediate: false }) async function discardDraft(id: string) { await deleteDraft(id) @@ -199,7 +240,7 @@ const statusLabel = (status: unknown) => { -
+
{{ t('dashboard.epics') }} @@ -233,6 +274,17 @@ const statusLabel = (status: unknown) => { + + + QA + + + +
{{ qaPlans.length }}
+

Planes QA generados

+
+
+ {{ t('dashboard.sessions') }} @@ -439,7 +491,14 @@ const statusLabel = (status: unknown) => { > - Enviar + {{ d.type === 'E' ? 'Epica' : 'HU' }} + +