docs: M5d-1 and M5d-2 implementation plans for Ornith

Eight closed tasks for epochs and compaction, then ten for the scheduler,
ToolSummary, and tiers. Given tests first. Gate 1456 → 1507 → 1572.

Co-Authored-By: Grok 4.6 <noreply@x.ai>
This commit is contained in:
2026-10-02 21:54:37 -07:00
co-authored by Grok 4.6
parent 069cd1686a
commit 3726ae585f
48 changed files with 7319 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
# M5d-2 task 04: five-field UTC cron matcher
**Branch:** `m5d2`. Check it: `git branch --show-current` prints `m5d2` and `git status --short`
is empty; otherwise stop. Never switch branch.
**Commit subject:** `loopd: five-field UTC cron matcher`
**Log row:** the Task cell is `M5d2/04-loopd-cron`.
## Goal
`loopd::cron::due(schedule, Timestamp) -> Result<bool, CronError>` (spec 4.2). Five fields:
minute hour day-of-month month day-of-week. Each field is `*`, a single number, `n-m`, or
`*/n`. Day-of-week `0` is Sunday. No cron crate. DOM and DOW are AND (README known limits).
Nothing loads job files yet.
## Every place that changes
| Place | What happens there |
|---|---|
| `crates/loopd/src/cron.rs` | new: `due`, `CronError` |
| `crates/loopd/src/lib.rs` | `pub mod cron;` |
| `crates/loopd/tests/cron.rs` | new, given |
| everything else | **no change** |
## Files
- Copy, from `docs/plans/M5d2/files/` (the same path under it): the new test
`crates/loopd/tests/cron.rs`.
- Create: `crates/loopd/src/cron.rs` (skeleton below).
- Modify: `crates/loopd/src/lib.rs`, `docs/implementer-log.md`.
- Do not touch: any other file.
## Facts you need
- **Civil fields from RFC 3339.** `Timestamp::to_rfc3339` is `YYYY-MM-DDTHH:MM:SS.mmmZ`.
Minute = `rfc[14..16]`, hour = `rfc[11..13]`, day = `rfc[8..10]`, month = `rfc[5..7]`,
parsed as `u32`. If a slice is missing, `CronError::Fields` naming the schedule.
- **Weekday.** `0` is Sunday. `(unix_millis() / 1000 / 86400 + 4) % 7` as `u32` (`1970-01-01`
was Thursday, so `+ 4`). `unix_millis` is `pub` on `Timestamp`.
- **Ranges.** Inclusive. `*/n` matches when `value % n == 0` (multiples from 0). `n == 0` in
a step is `Fields`.
- **Bounds.** Minute 0–59, hour 0–23, DOM 1–31, month 1–12, DOW 0–6. A number outside its
field's range is `Fields`. Four or six fields, `@daily`, and any other token are `Fields`.
- **`Display`.** Names the schedule, e.g. contains `x x x x x` when that was the input. Do not
add a runbook pointer; this error is used by job loading in task 05.
- **Split.** `split_whitespace`. Five pieces or `Fields`.
- rustfmt decides `mod` order in `lib.rs`. After `pub mod compact;` alphabetically `cron`
comes after `config` and before `http`. Insert where rustfmt will put it; run `cargo fmt`
before the gate.
- The items the edits use. This table is complete.
| Item | What it does |
|---|---|
| `Timestamp::to_rfc3339` | civil fields |
| `Timestamp::unix_millis` | weekday |
| `str::parse::<u32>` | numbers |
## Skeleton
`crates/loopd/src/cron.rs`:
```rust
//! Five-field UTC crontab matcher (M5d spec, section 4.2). No cron crate.
use bxcore::Timestamp;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CronError {
Fields(String),
}
impl std::fmt::Display for CronError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CronError::Fields(schedule) => {
write!(f, "cron schedule {schedule:?} is not five UTC fields")
}
}
}
}
impl std::error::Error for CronError {}
/// Whether `schedule` matches `now` in UTC.
pub fn due(schedule: &str, now: Timestamp) -> Result<bool, CronError> {
let fields: Vec<&str> = schedule.split_whitespace().collect();
if fields.len() != 5 {
return Err(CronError::Fields(schedule.to_string()));
}
let rfc = now.to_rfc3339();
let minute = number(&rfc, 14, 16, schedule)?;
let hour = number(&rfc, 11, 13, schedule)?;
let day = number(&rfc, 8, 10, schedule)?;
let month = number(&rfc, 5, 7, schedule)?;
let dow = {
let days = now.unix_millis() / 1000 / 86_400;
((days + 4) % 7) as u32
};
Ok(match_field(fields[0], minute, 0, 59, schedule)?
&& match_field(fields[1], hour, 0, 23, schedule)?
&& match_field(fields[2], day, 1, 31, schedule)?
&& match_field(fields[3], month, 1, 12, schedule)?
&& match_field(fields[4], dow, 0, 6, schedule)?)
}
fn number(rfc: &str, start: usize, end: usize, schedule: &str) -> Result<u32, CronError> {
let piece = rfc
.get(start..end)
.ok_or_else(|| CronError::Fields(schedule.to_string()))?;
piece
.parse()
.map_err(|_| CronError::Fields(schedule.to_string()))
}
fn match_field(
field: &str,
value: u32,
min: u32,
max: u32,
schedule: &str,
) -> Result<bool, CronError> {
let err = || CronError::Fields(schedule.to_string());
if field == "*" {
return Ok(true);
}
if let Some(step) = field.strip_prefix("*/") {
let n: u32 = step.parse().map_err(|_| err())?;
if n == 0 {
return Err(err());
}
return Ok(value.is_multiple_of(n));
}
if let Some((a, b)) = field.split_once('-') {
let lo: u32 = a.parse().map_err(|_| err())?;
let hi: u32 = b.parse().map_err(|_| err())?;
if lo < min || hi > max || lo > hi {
return Err(err());
}
return Ok(value >= lo && value <= hi);
}
let n: u32 = field.parse().map_err(|_| err())?;
if n < min || n > max {
return Err(err());
}
Ok(n == value)
}
```
`u32::is_multiple_of` is stable in 1.98. Do not use `%` with a comment that `n` was checked;
the skeleton already returns `Fields` for `n == 0`.
If `is_multiple_of` is not in scope on `u32` in this toolchain, use `value % n == 0` after the
`n == 0` check, and stop only if both fail to compile.
## Steps
- [ ] **1. Copy the given test.**
```sh
cp docs/plans/M5d2/files/crates/loopd/tests/cron.rs crates/loopd/tests/
```
- [ ] **2. See it fail.** `cargo build --tests --keep-going -p loopd`, timeout 600000 ms: test
target `cron` does not compile (`E0432` unresolved import `loopd::cron`). Every other
target compiles.
- [ ] **3. The module.** Write `cron.rs` from the skeleton. Add `pub mod cron;` to `lib.rs`
where rustfmt will put it. `cargo check -p loopd` prints no error and no warning.
- [ ] **4. The tests.** `cargo test -p loopd --test cron`: every test passes, 10 tests.
- [ ] **5. Check.** `git diff --stat` lists `crates/loopd/src/cron.rs` and
`crates/loopd/src/lib.rs`.
- [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line
`gate: ok`. Test count **1535** (1525 before, plus 10).
- [ ] **7. Log and commit.** Task cell `M5d2/04-loopd-cron`. Stage with
`git add crates/loopd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says.
## Done when
- `make gate` prints `gate: ok` with 1535 tests.
- `git diff HEAD~1 --stat` lists only `crates/loopd/src/cron.rs`, `crates/loopd/src/lib.rs`,
`crates/loopd/tests/cron.rs` and the log.
## Stop and report if
- The branch check fails; step 2 differs; `cargo check` warns; a test fails; the gate count is
not 1535; you add a dependency; you treat DOM and DOW as OR; you accept `@daily` or a sixth
field; `cron.rs` exceeds 500 lines.
## What the given tests pin
`0 7 * * *` matches that minute only; `* * * * *` matches; `*/5` is multiples from 0; `0-10`
is inclusive; Sunday is 0 on 2026-10-04; the 1st of the month; four or six fields error;
minute 60, hour 24, DOW 7 error; `@daily` errors; `Display` names the schedule.