{"record":{"id":"f7154d5dbb575ad8","repo":"Hmbown/CodeWhale","slug":"weekly-schedules-require-byhour","errorCode":null,"errorMessage":"WEEKLY schedules require BYHOUR","messagePattern":"WEEKLY schedules require BYHOUR","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/automation_manager.rs","lineNumber":375,"sourceCode":"            }\n            AutomationFrequency::Weekly => {\n                for key in parts.keys() {\n                    if key != \"FREQ\" && key != \"BYDAY\" && key != \"BYHOUR\" && key != \"BYMINUTE\" {\n                        bail!(\n                            \"Unsupported RRULE field '{key}' for WEEKLY. Allowed: FREQ,BYDAY,BYHOUR,BYMINUTE\"\n                        );\n                    }\n                }\n                let byday_raw = parts\n                    .get(\"BYDAY\")\n                    .ok_or_else(|| anyhow::anyhow!(\"WEEKLY schedules require BYDAY\"))?;\n                let byday = parse_byday(&byday_raw.to_ascii_uppercase())?;\n                if byday.is_empty() {\n                    bail!(\"BYDAY cannot be empty for WEEKLY schedules\");\n                }\n                let byhour = parts\n                    .get(\"BYHOUR\")\n                    .ok_or_else(|| anyhow::anyhow!(\"WEEKLY schedules require BYHOUR\"))?\n                    .parse::<u32>()\n                    .context(\"Failed to parse BYHOUR\")?;\n                let byminute = parts\n                    .get(\"BYMINUTE\")\n                    .ok_or_else(|| anyhow::anyhow!(\"WEEKLY schedules require BYMINUTE\"))?\n                    .parse::<u32>()\n                    .context(\"Failed to parse BYMINUTE\")?;\n\n                if byhour > 23 {\n                    bail!(\"BYHOUR must be between 0 and 23\");\n                }\n                if byminute > 59 {\n                    bail!(\"BYMINUTE must be between 0 and 59\");\n                }\n\n                Ok(Self::Weekly {\n                    byday,\n                    byhour,","sourceCodeStart":357,"sourceCodeEnd":393,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/automation_manager.rs#L357-L393","documentation":"AutomationSchedule::parse_rrule requires WEEKLY schedules to pin the fire hour via BYHOUR. BYDAY is checked first, so this error means BYDAY parsed fine but the BYHOUR key is absent; WEEKLY supports only FREQ,BYDAY,BYHOUR,BYMINUTE and the hour cannot default.","triggerScenarios":"An automation rrule like 'FREQ=WEEKLY;BYDAY=MO;BYMINUTE=30' (BYHOUR omitted) reaching AutomationSchedule::parse_rrule through automation create/update or the runtime API.","commonSituations":"Copying a weekly RRULE that was truncated; assuming BYHOUR defaults like it does for HOURLY anchors (it does not: WEEKLY requires it); editing a stored rrule by hand and dropping the field.","solutions":["Add BYHOUR as 0-23: FREQ=WEEKLY;BYDAY=MO,WE;BYHOUR=9;BYMINUTE=30","Note the value must parse as u32 and be <= 23, or you get the follow-up 'BYHOUR must be between 0 and 23' error","Validate with parse_rrule before persisting the automation"],"exampleFix":"// before\nlet rrule = \"FREQ=WEEKLY;BYDAY=MO,WE;BYMINUTE=30\";\n// after\nlet rrule = \"FREQ=WEEKLY;BYDAY=MO,WE;BYHOUR=9;BYMINUTE=30\";","handlingStrategy":"validation","validationCode":"let parts: std::collections::BTreeMap<&str, &str> = rrule\n    .split(';')\n    .filter_map(|kv| kv.split_once('='))\n    .collect();\nif parts.get(\"FREQ\").copied() == Some(\"WEEKLY\") {\n    let byhour = parts.get(\"BYHOUR\").context(\"WEEKLY requires BYHOUR\")?;\n    let hour: u32 = byhour.parse().context(\"BYHOUR must be numeric\")?;\n    ensure!(hour <= 23, \"BYHOUR must be 0-23\");\n}","typeGuard":"fn weekly_rrule_has_valid_byhour(rrule: &str) -> bool {\n    rrule.split(';').any(|kv| {\n        let (k, v) = kv.split_once('=').unwrap_or((\"\", \"\"));\n        k.eq_ignore_ascii_case(\"FREQ\") && v.eq_ignore_ascii_case(\"WEEKLY\")\n    }) && rrule\n        .split(';')\n        .find_map(|kv| kv.split_once('=').filter(|(k, _)| k == \"BYHOUR\").map(|(_, v)| v))\n        .and_then(|v| v.parse::<u32>().ok())\n        .is_some_and(|h| h <= 23)\n}","tryCatchPattern":null,"preventionTips":["WEEKLY has no default hour — unlike HOURLY anchors, BYHOUR is mandatory","Range-check BYHOUR (0-23) before saving; out-of-range fails later with a less specific error","Use a single shared RRULE builder/validator so all writers emit the same field set","Test schedule strings against parse_rrule in unit tests for every FREQ you support"],"tags":["automation","rrule","scheduling","validation"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}