Summary
chrono_now() in crates/larql-vindex/src/extract/build_helpers/timestamp.rs uses a hand-rolled day-to-calendar conversion that is systematically wrong, causing extracted_at in index.json to drift from the actual extraction date.
Root cause
let days = secs / 86400;
let years_approx = 1970 + days / 365; // ignores leap years
let remainder_days = days % 365;
let months = remainder_days / 30 + 1; // all months treated as 30 days
let day = remainder_days % 30 + 1;
Two compounding errors:
- Leap years ignored —
days / 365 accumulates ~1 day of error per 4 years (14 extra days by 2026).
- Month lengths wrong —
remainder_days / 30 treats every month as 30 days. Months are 28–31 days, so the month and day within month are both wrong.
Observed impact
A vindex extracted on 2026-06-16 (confirmed by filesystem mtime) has "extracted_at": "2026-07-01T14:24:16Z" — 15 days in the future. The error grows over time.
Existing tests do not catch this
The tests in timestamp.rs only verify format shape (length, separators, year ≥ 2020 and month/day in range). They do not verify that the emitted date matches the actual wall-clock date.
Suggested fix
Replace the approximation with Howard Hinnant's civil calendar algorithm (O(1), no extra dependencies, exact for all Gregorian dates):
pub(crate) fn chrono_now() -> String {
let d = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = d.as_secs();
let (year, month, day) = days_since_epoch_to_date(secs / 86400);
let hour = (secs % 86400) / 3600;
let min = (secs % 3600) / 60;
let sec = secs % 60;
format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", year, month, day, hour, min, sec)
}
fn days_since_epoch_to_date(days: u64) -> (u64, u64, u64) {
// Hinnant civil calendar: https://howardhinnant.github.io/date_algorithms.html
let z = days as i64 + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe/1460 + doe/36524 - doe.min(146096)/146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365*yoe + yoe/4 - yoe/100);
let mp = (5*doy + 2) / 153;
let d = doy - (153*mp + 2)/5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as u64, m, d)
}
The existing tests should be extended to verify the emitted date matches SystemTime::now() (or at least that the year/month match within a small tolerance).
Files
crates/larql-vindex/src/extract/build_helpers/timestamp.rs
Summary
chrono_now()incrates/larql-vindex/src/extract/build_helpers/timestamp.rsuses a hand-rolled day-to-calendar conversion that is systematically wrong, causingextracted_atinindex.jsonto drift from the actual extraction date.Root cause
Two compounding errors:
days / 365accumulates ~1 day of error per 4 years (14 extra days by 2026).remainder_days / 30treats every month as 30 days. Months are 28–31 days, so the month and day within month are both wrong.Observed impact
A vindex extracted on 2026-06-16 (confirmed by filesystem
mtime) has"extracted_at": "2026-07-01T14:24:16Z"— 15 days in the future. The error grows over time.Existing tests do not catch this
The tests in
timestamp.rsonly verify format shape (length, separators, year ≥ 2020 and month/day in range). They do not verify that the emitted date matches the actual wall-clock date.Suggested fix
Replace the approximation with Howard Hinnant's civil calendar algorithm (O(1), no extra dependencies, exact for all Gregorian dates):
The existing tests should be extended to verify the emitted date matches
SystemTime::now()(or at least that the year/month match within a small tolerance).Files
crates/larql-vindex/src/extract/build_helpers/timestamp.rs