Files
craft-libs/AGENTS.md
Dylan M. Taylor f180fc0d35 Use chrono instead of hand-rolled UTC conversion (#219)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled civil_from_days and Day::to_days in
crates/ai-core/src/history/report.rs with chrono, added as a workspace
dependency (std-only, so ai-core still compiles for wasm). The day/month
grouping, ISO and CSV timestamps and the Day/Month display are byte-identical
and pinned by the existing tests. AGENTS.md now says time goes through chrono.

Follow-up to storytold/cadcraft#1785

* history: preserve out-of-range conversion failure in civil_from_days

civil_from_days used `i32::try_from(..).unwrap_or(0)` before calling
NaiveDate::from_num_days_from_ce_opt, so a CE day count past i32 (e.g.
from Day::from_days(i64::MAX)) passed a valid day 0 to chrono and read
as 0000-12-31 instead of the 0000-01-01 fallback the None arm gives for
every other out-of-range date. Convert with `.ok().and_then(..)` so both
the integer-conversion and NaiveDate-construction failures share the one
fallback. Regression test covers i64::MAX and i64::MIN.

Doc comments on the chrono-touched to_days/from_days/civil_from_days now
state the out-of-range behaviour (0 / 0000-01-01) for the docstring gate.

* Convert every i32 year, not only chrono's range

Day holds any i32 year, but NaiveDate stops near ±262143, so years like
262144 read as 0 / 0000-01-01. The Gregorian calendar repeats every 400
years: chrono now converts the day within its cycle and whole cycles are
added back, in both directions. Values match the pre-chrono formula
(tested for 262144, -262145, i32::MAX and i32::MIN). Nonexistent days
still read as 0, and day counts whose year doesn't fit i32 still read as 0000-01-01.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-23 11:17:51 -04:00

1.4 KiB

AGENTS.md

Shared Rust libraries for the Craft Apps. The shared standards live in craftrules (../../craftrules on a dev machine): read its AGENTS.md, then standards/never-crash.md, standards/engineering.md and standards/licensing.md.

  • Never crash. Every crate's lib.rs starts with #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]. No unsafe (the workspace forbids it).
  • New crates go in crates/<name>, named craft-<name>, inherit the workspace package fields and lints, and are listed in [workspace.dependencies].
  • No app code. A crate here must be useful to more than one app and must not depend on any app.
  • Time goes through chrono (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic).
  • Before merging: cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace.