Sistema de códigos jerárquicos 2-niveles + asignación determinista post-IA

- hierarchy.ts: Spike (S) agregado, buildHierarchyPath genera [E01-F04] (2 niveles)
  Legacy [E05-F04-U01] preservado (regex opcional 3er segmento)
- hierarchy-generator.ts (nuevo): analyzeExisting() computa contadores por épica+tipo
  assignEpicCodes() asigna E{max+1} secuencial
  assignItemCodes() asigna {epic}-{tipo}{n+1} a cada HU dentro de su épica
- project-analyzer.ts: post-procesa épicas y HUs con generador de códigos
  saveEpicDrafts usa epicCode en metadata y título con [E01]
- prompts-db.ts: prompt FASE 2 instruye a la IA a no generar códigos
- workitems.ts: EnrichedEpic._epicCode, EnrichedUserStory._epicCode/_itemCode
- DashboardView: muestra códigos en drafts y tabla de épicas
This commit is contained in:
2026-05-29 18:13:17 -05:00
parent 9cf12b482f
commit 3035351e6f
6 changed files with 218 additions and 26 deletions
+29 -6
View File
@@ -2,7 +2,8 @@ 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 type { EnrichedUserStory } from '@/stores/workitems'
import { analyzeExisting, assignEpicCodes, assignItemCodes } from '@/services/hierarchy-generator'
import type { EnrichedEpic, EnrichedUserStory } from '@/stores/workitems'
export interface AnalysisHU {
title: string
@@ -70,8 +71,9 @@ export async function analyzeProjectEpics(
projectId: number,
projectName: string,
existingHUs: EnrichedUserStory[],
existingEpics: string[], // épicas que ya están en KAPPA
existingEpics: string[], // nombres de épicas que ya están en KAPPA
signal?: AbortSignal,
existingEpicItems?: EnrichedEpic[], // épicas enriquecidas para contar códigos
): Promise<AnalysisEpicsResult> {
const context = await buildProjectContext(projectId, projectName, existingHUs)
@@ -101,8 +103,14 @@ IMPORTANTE: Respondé SOLO con épicas en esta fase. NO generes HUs aún.`
try {
const jsonStr = extractJSON(content)
const parsed = JSON.parse(jsonStr)
const rawEpics: AnalysisEpic[] = parsed.epics || parsed.epic || []
// Asignar códigos jerárquicos (E06, E07...) a las épicas propuestas
const counters = analyzeExisting(existingEpicItems || [], existingHUs)
const epicsWithCodes = assignEpicCodes(rawEpics, counters)
const result: AnalysisEpicsResult = {
epics: parsed.epics || parsed.epic || [],
epics: epicsWithCodes,
summary: parsed.summary || '',
rationale: parsed.rationale || parsed.summary || '',
}
@@ -153,11 +161,21 @@ IMPORTANTE: Respondé SOLO con HUs en esta fase. NO generes épicas nuevas.`
try {
const jsonStr = extractJSON(content)
const parsed = JSON.parse(jsonStr)
const rawHUs: AnalysisHU[] = parsed.hus || []
// Asignar códigos jerárquicos (E01-F04, E01-T01...) a las HUs propuestas
const counters = analyzeExisting([], existingHUs)
const epicsWithCodes = confirmedEpics.map(e => ({
name: e.name,
epicCode: (e as any).epicCode || '',
}))
const husWithCodes = assignItemCodes(rawHUs, epicsWithCodes, counters)
const result: AnalysisHUsResult = {
hus: parsed.hus || [],
hus: husWithCodes,
summary: parsed.summary || '',
}
console.log(`[Alpha] Phase 2 complete: ${result.hus.length} HUs generadas`)
console.log(`[Alpha] Phase 2 complete: ${result.hus.length} HUs generadas con códigos`)
return result
} catch (e) {
console.error('[Alpha] Failed to parse HUs analysis. Raw:', content)
@@ -176,18 +194,23 @@ export async function saveEpicDrafts(
let skipped = 0
for (const epic of epics) {
const epicWithCode = epic as any
const epicCode = epicWithCode.epicCode || ''
const fullTitle = epicCode ? `[${epicCode}] ${epic.name}` : epic.name
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,
id: createDraftId(), projectId, title: fullTitle,
description: epic.description, acceptanceCriteria: '',
priority: 'Media', type: 'E',
metadata: JSON.stringify({
linkedHuTitles: epic.linkedHuTitles,
estimatedStart: epic.estimatedStart,
estimatedEnd: epic.estimatedEnd,
epicCode,
}),
syncStatus: 'draft', createdAt: new Date().toISOString(),
})