package config import ( "errors" "fmt" "time" "github.com/kami/maven/internal/morning" "github.com/robfig/cron/v3" ) // The two scheduled things Maven says on her own, plus the window she does not // say them in. Both routine kinds default to severity 1 — the care class, which // quiet hours suppress and which never reaches an away channel. A routine that // speaks at 3am has to be written that way on purpose. // RoutineConfig — one scheduled routine. Cron is a standard 5-field expression // ("0 8 * * *" = 08:00 daily). Body is the RU text delivered verbatim (routines // are not LLM-phrased). Severity (1-4, default 1) drives routing: care-class // (≤2) is suppressed by quiet hours and drops when away; ops-class reaches away // channels. type RoutineConfig struct { Name string `json:"name"` Cron string `json:"cron"` Body string `json:"body"` Severity int `json:"severity,omitempty"` } // MorningRoutineConfig — one daily checklist. WindowStart/WindowEnd/NudgeAt // are "HH:MM" local time; NudgeAt empty defaults to WindowEnd. Weekdays are // 0=Sunday..6=Saturday; empty means every day (set two routines under // different names for weekday/weekend variants). type MorningRoutineConfig struct { Name string `json:"name"` Weekdays []int `json:"weekdays,omitempty"` WindowStart string `json:"window_start"` WindowEnd string `json:"window_end"` NudgeAt string `json:"nudge_at,omitempty"` Severity int `json:"severity,omitempty"` Items []MorningRoutineItemConfig `json:"items"` } // MorningRoutineItemConfig — one checklist entry. FactKey is the fact whose // presence within the window counts as completion evidence. type MorningRoutineItemConfig struct { Key string `json:"key"` FactKey string `json:"fact_key"` Label string `json:"label"` // Optional — this one being skipped does not earn a nudge. Default false, // so a routine written before 04-08-2026 keeps behaving as it did. Optional bool `json:"optional,omitempty"` } // QuietHoursConfig — a recurring daily quiet-window. Times are local to the // server's wall clock. A window crossing midnight (Start > End) is handled: // "23:00"-"08:00" means quiet from 23:00 to 08:00 the next day. type QuietHoursConfig struct { Start string `json:"start,omitempty"` // "HH:MM" local time, e.g. "23:00" End string `json:"end,omitempty"` // "HH:MM" local time, e.g. "08:00" } // normaliseRoutines defaults both routine kinds to severity 1, the care class: // the safe floor, so a misconfigured routine cannot blast an away channel at // 3am. func (c *Config) normaliseRoutines() { for i := range c.Routines { if c.Routines[i].Severity == 0 { c.Routines[i].Severity = 1 } } for i := range c.MorningRoutines { if c.MorningRoutines[i].Severity == 0 { c.MorningRoutines[i].Severity = 1 } } } // validateRoutines fails a routine that could never fire. A typo in a cron // expression or a missing body should stop the daemon at startup, not go // unnoticed as silence at 08:00 every day. func (c *Config) validateRoutines() error { for _, r := range c.Routines { if r.Name == "" { return errors.New("routine: name is required") } if r.Body == "" { return fmt.Errorf("routine %q: body is required", r.Name) } if _, err := cron.ParseStandard(r.Cron); err != nil { return fmt.Errorf("routine %q: bad cron %q: %w", r.Name, r.Cron, err) } } if len(c.MorningRoutines) > 0 { if err := morning.Validate(morningRoutinesFromConfig(c.MorningRoutines)); err != nil { return err } } return nil } // morningRoutinesFromConfig maps the config's morning-routine blocks to the // engine type. Shared with the daemon so config validation and daemon wiring // can never drift on the mapping. func morningRoutinesFromConfig(mc []MorningRoutineConfig) []morning.Routine { out := make([]morning.Routine, len(mc)) for i, r := range mc { items := make([]morning.Item, len(r.Items)) for j, it := range r.Items { items[j] = morning.Item{Key: it.Key, FactKey: it.FactKey, Label: it.Label, Optional: it.Optional} } weekdays := make([]time.Weekday, len(r.Weekdays)) for j, w := range r.Weekdays { weekdays[j] = time.Weekday(w) } out[i] = morning.Routine{ Name: r.Name, Weekdays: weekdays, WindowStart: r.WindowStart, WindowEnd: r.WindowEnd, NudgeAt: r.NudgeAt, Severity: r.Severity, Items: items, } } return out } // MorningRoutinesFromConfig is the exported form daemon wiring uses. func MorningRoutinesFromConfig(mc []MorningRoutineConfig) []morning.Routine { return morningRoutinesFromConfig(mc) }