# Scheduling / Cron Gap identificado en el registry. Reimplementar desde cero. Los pipelines se lanzan manualmente o desde la TUI. No hay scheduling recurrente dentro del registry. Complementario a Dagu — estas funciones son primitivas componibles, no un scheduler completo. ## Funciones a implementar ### parse_cron_expr - **Dominio:** core - **Lang:** Go - **Purity:** pure - **Signature:** `ParseCronExpr(expr string) (CronSchedule, error)` - **Retorno:** CronSchedule con campos Minute, Hour, DayOfMonth, Month, DayOfWeek parseados - **Descripcion:** Parser de expresiones cron estandar (5 campos). Soporta `*`, rangos (`1-5`), listas (`1,3,5`), pasos (`*/15`), y aliases (@hourly, @daily, @weekly, @monthly). No soporta seconds ni years (no es Quartz). - **Algoritmo:** 1. Check aliases (@hourly → "0 * * * *", etc) 2. Split por espacios, validar 5 campos 3. Por campo: expandir `*` a rango completo, parsear rangos/listas/pasos 4. Validar limites (minute 0-59, hour 0-23, etc) - **Deps:** `strings`, `strconv`, `fmt` (solo stdlib) - **Tests:** - "*/15 * * * *" → minutos [0,15,30,45] - "0 9 * * 1-5" → 9am weekdays - "@daily" → "0 0 * * *" - "0 9 1,15 * *" → dia 1 y 15 a las 9 - Expresion invalida → error descriptivo - Campo fuera de rango → error ### next_cron_time - **Dominio:** core - **Lang:** Go - **Purity:** pure - **Signature:** `NextCronTime(schedule CronSchedule, after time.Time) time.Time` - **Retorno:** Proximo time.Time que cumple el schedule despues de `after` - **Descripcion:** Calcula la proxima ejecucion de un cron schedule. Avanza minuto a minuto desde `after` hasta encontrar un match. Limit de 366 dias para evitar loops infinitos en schedules imposibles. - **Algoritmo:** 1. Truncar `after` al minuto, avanzar 1 minuto 2. Check month → si no match, avanzar al primer dia del proximo mes valido 3. Check day of month y day of week → si no match, avanzar al proximo dia 4. Check hour → si no match, avanzar a la proxima hora 5. Check minute → si no match, avanzar al proximo minuto - **Deps:** `time` - **Tests:** - "0 * * * *" desde 14:30 → 15:00 - "0 9 * * 1" desde viernes → proximo lunes 9:00 - "0 0 29 2 *" → proximo 29 de febrero - Schedule imposible → panic o error despues de limit ### cron_ticker - **Dominio:** infra - **Lang:** Go - **Purity:** impure (goroutine + channel + time) - **Signature:** `CronTicker(schedule CronSchedule, ctx context.Context) <-chan time.Time` - **Retorno:** Channel que emite un time.Time en cada tick del schedule - **Descripcion:** Crea un ticker que emite en los momentos definidos por el cron schedule. Usa time.NewTimer internamente, recalculando el proximo tick despues de cada emision. Se detiene al cancelar el context. - **Deps:** `time`, `context` - **Tests:** - Ticker con schedule "* * * * *" emite cada minuto (test con clock mock o schedule rapido) - Context cancel detiene el ticker - Channel se cierra al cancelar ### parse_cron_expr (Python) - **Dominio:** core - **Lang:** Python - **Purity:** pure - **Signature:** `parse_cron_expr(expr: str) -> dict` - **Retorno:** `{"minute": list[int], "hour": list[int], "day_of_month": list[int], "month": list[int], "day_of_week": list[int]}` - **Descripcion:** Misma semantica que Go. Dict en vez de struct. - **Tests:** Mismos casos que Go ### next_cron_time (Python) - **Dominio:** core - **Lang:** Python - **Purity:** pure - **Signature:** `next_cron_time(schedule: dict, after: datetime) -> datetime` - **Descripcion:** Misma semantica que Go. - **Tests:** Mismos casos que Go ## Tipo a implementar ### CronSchedule (Go) - **Dominio:** core - **Lang:** Go - **Algebraic:** product - **Definicion:** ```go type CronSchedule struct { Minute []int Hour []int DayOfMonth []int Month []int DayOfWeek []int Raw string // expresion original } ```