3035351e6f
- 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
114 lines
3.1 KiB
TypeScript
114 lines
3.1 KiB
TypeScript
export type ItemType = 'E' | 'F' | 'U' | 'T' | 'B' | 'S'
|
|
|
|
export interface HierarchyInfo {
|
|
fullPath: string
|
|
items: { type: ItemType; number: number }[]
|
|
epicCode: string | null
|
|
itemCode: string | null
|
|
}
|
|
|
|
const TYPE_LABELS: Record<ItemType, string> = {
|
|
E: 'Épica',
|
|
F: 'Feature',
|
|
U: 'HU',
|
|
T: 'Tarea',
|
|
B: 'Bug',
|
|
S: 'Spike',
|
|
}
|
|
|
|
const TYPE_COLORS: Record<ItemType, string> = {
|
|
E: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
|
F: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
|
U: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',
|
|
T: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
|
|
B: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400',
|
|
S: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400',
|
|
}
|
|
|
|
const TYPE_ICONS: Record<ItemType, string> = {
|
|
E: '📦',
|
|
F: '⭐',
|
|
U: '📋',
|
|
T: '⚙️',
|
|
B: '🐛',
|
|
S: '🔬',
|
|
}
|
|
|
|
export function getTypeLabel(type: ItemType): string {
|
|
return TYPE_LABELS[type] || type
|
|
}
|
|
|
|
export function getTypeColor(type: ItemType): string {
|
|
return TYPE_COLORS[type] || 'bg-muted text-muted-foreground'
|
|
}
|
|
|
|
export function getTypeIcon(type: ItemType): string {
|
|
return TYPE_ICONS[type] || '📄'
|
|
}
|
|
|
|
const HIERARCHY_REGEX = /\[(([EFUTBS]\d+)(?:-([EFUTBS]\d+))?(?:-([EFUTBS]\d+))?)\]/i
|
|
|
|
export function parseHierarchy(title: string): HierarchyInfo | null {
|
|
const match = title.match(HIERARCHY_REGEX)
|
|
if (!match) return null
|
|
|
|
const fullPath = match[1].toUpperCase()
|
|
const segments = [match[2]]
|
|
if (match[3]) segments.push(match[3])
|
|
if (match[4]) segments.push(match[4])
|
|
|
|
const items = segments.map(s => ({
|
|
type: s[0] as ItemType,
|
|
number: parseInt(s.slice(1), 10),
|
|
}))
|
|
|
|
const epic = items.find(i => i.type === 'E')
|
|
|
|
return {
|
|
fullPath,
|
|
items,
|
|
epicCode: epic ? `E${String(epic.number).padStart(2, '0')}` : null,
|
|
itemCode: items.length > 1 ? `${items[1].type}${String(items[1].number).padStart(2, '0')}` : null,
|
|
}
|
|
}
|
|
|
|
export function stripHierarchy(title: string): string {
|
|
return title.replace(HIERARCHY_REGEX, '').trim()
|
|
}
|
|
|
|
export function getItemType(title: string): ItemType {
|
|
const h = parseHierarchy(title)
|
|
if (h?.items.length) return h.items[h.items.length - 1].type
|
|
return 'U'
|
|
}
|
|
|
|
export function buildHierarchyPath(
|
|
epicNum?: number,
|
|
itemType?: ItemType,
|
|
itemNum?: number,
|
|
): string {
|
|
if (epicNum === undefined) return ''
|
|
const parts: string[] = [`E${String(epicNum).padStart(2, '0')}`]
|
|
if (itemType && itemNum !== undefined) {
|
|
parts.push(`${itemType}${String(itemNum).padStart(2, '0')}`)
|
|
}
|
|
return `[${parts.join('-')}]`
|
|
}
|
|
|
|
export function buildFullTitle(
|
|
name: string,
|
|
epicNum?: number,
|
|
itemType?: ItemType,
|
|
itemNum?: number,
|
|
): string {
|
|
const path = buildHierarchyPath(epicNum, itemType, itemNum)
|
|
return path ? `${path} ${name}` : name
|
|
}
|
|
|
|
export function extractItemNumber(title: string, type: ItemType): number | null {
|
|
const h = parseHierarchy(title)
|
|
if (!h) return null
|
|
const found = h.items.find(i => i.type === type)
|
|
return found?.number ?? null
|
|
}
|