docs/gaps.md row 4: one row (t06 had kept a second, older copy of it); t05's #1840 and t06's #2964 clauses both in, main's #2963 clause kept. docs/recovery.md: t06's retry sentence with t01's base64 size text. Test files: both tracks' tests kept whole.
crates/pdf/src/lib.rs: t01's vector_text kept before t05's watermark (box area signature). Test files and canvas.rs tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
docs/file-format-parity.md: main's status line with this change added; both progress-log rows, this one first.
docs/localization-parity.md: main's status line with this change added.
crates/formats/src/odt.rs: StyleInfo keeps both cell_valign (#2416) and column_width.
ROADMAP.md, docs/file-format-parity.md, docs/gaps.md: main's status lines kept; the PR's log rows added.
crates/formats/src/odt.rs: Body::new from #873 with main's xml_space_end; parse_tracked returns main's document font as 4th element and #873's page parts as 5th (the reviewed integration fix for #873).
docs/file-format-parity.md: status line, ODT row and change log carry both sides.
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled civil_from_days / days_from_civil copies and the
Excel serial-date, FILETIME and logging/PDF date conversions with chrono as
a workspace dependency (default-features = false, std only, so wasm is
unaffected). Output formats are unchanged and pinned by the existing (and one
new) test. Excel's 1900 leap-year bug and the 1900/1904 systems keep their
explicit special cases; a small proleptic-Gregorian fallback covers years
outside chrono's NaiveDate range so no input changes behaviour or panics.
AGENTS.md now says time goes through chrono.
Follow-up to storytold/cadcraft#1785
* Never panic or reshape date output for clocks outside chrono's range
chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. logging::timestamp now uses the fallible
DateTime::from_timestamp and reads such a clock as the epoch, as the old
saturating code did.
iso_from_unix_secs fell back to the Unix epoch for timestamps chrono
rejects; it now keeps the proleptic Gregorian date civil_from_days gives,
restoring the pre-chrono result for supported out-of-range u64 inputs.
Years past 9999 are written with {:04} rather than %Y (which prepends a
'+') in logging::timestamp, iso_from_unix_secs, docbin::filetime_iso and
odt::now_iso, so byte-for-byte output is preserved.
filetime_iso gained a docstring; AGENTS.md's chrono bullet now notes the
narrow out-of-range fallback. Tests cover each boundary.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* Route out-of-range dates through chrono's 400-year cycle, not hand-rolled copies
The PR kept hand-rolled Hinnant `civil_fallback`/`days_from_civil_fallback`
copies for dates outside chrono's ±262143-year range, and AGENTS.md carved a
"narrow fallback" exception for them. That left calendar arithmetic outside
chrono after all.
Replace every copy with the Gregorian 400-year cycle (146_097 days): chrono
converts the day/year within one cycle and whole cycles are added back via i128
(so i64::MIN/MAX can't overflow). This keeps chrono as the only calendar logic
and reproduces the pre-chrono proleptic Gregorian result for every value:
- engine cmd/mod.rs `civil_from_days` (reached by the system clock and by
`iso_from_unix_secs` for a u64 seconds count past chrono's range).
- engine cmd/controls.rs `days_from_civil` and `days_in_month` (`days_in_month`
now uses `rem_euclid(400)` since month lengths repeat every 400 years).
- formats sheet.rs `civil` and docx chart.rs `civil` (Excel serials).
Outputs are unchanged: verified the cycle result equals the old formula over a
wide range and at the extremes with a scratch chrono program. New tests pin
accepted inputs past chrono's range on both sides of the epoch, through the
public/lib entry points and the private converters, with values from the old
formula. Removed the AGENTS.md "narrow fallback" exception.
pdf/lib.rs `civil` is left as-is: its only caller is the live system clock and
its year is clamped to 0..9999 by the PDF DateTime, so the epoch fallback past
year 262143 is the accepted live-clock behavior, not a hand-rolled copy.
Tested: cargo test -p wordcraft-engine -p wordcraft-formats -p wordcraft-docx;
fmt --check; clippy --all-targets -D warnings.
* logging/docbin/odt/pdf: keep extended-year dates, no epoch fallback
FredApps (CHANGES_REQUESTED) showed `DateTime::from_timestamp(..).unwrap_or_default()`
changes output: a timestamp the old code converted past chrono's year 262143 range now
collapsed to 1970-01-01 instead of its real proleptic-Gregorian date. The same regression
exists wherever the PR swapped a hand-rolled converter for the epoch fallback. Fix every
such site to keep chrono as the only calendar logic via the 400-year Gregorian cycle
(146_097 days): chrono converts the day within one cycle, whole cycles carry in the year
(i128, saturated into i64). Verified the cycle result equals the old formula across a wide
range and at the extremes.
- apps/wordcraft/src/logging.rs `timestamp`: added `civil_from_days(u64)` (cycle), kept the
pre-1970 epoch clamp (`duration_since`) and the milliseconds/time-of-day format exactly.
Pins: UNIX_EPOCH + 10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z (FredApps' example,
was 1970-...); the checked_add u64::MAX/2 s fixture now asserts 292277026596-12-04T15:30:07.000Z
instead of the epoch.
- crates/docbin/src/metadata.rs `filetime_iso`: restored a cycle-based `civil(i64)` instead
of `from_timestamp(..)?` (which dropped the field past chrono's range). A u64 FILETIME caps
at ~1.83e12 unix s (year 60056), inside chrono's range, so no reachable value changes; pinned
the largest FILETIME (u64::MAX) -> 60056-05-28T05:36:10Z to lock the extended-year path.
- crates/formats/src/odt.rs `now_iso`: reuse the crate's existing cycle converter
`sheet::civil` (made pub(crate)) rather than the epoch fallback.
- crates/pdf/src/lib.rs `civil`: cycle converter; year returned unclamped and the caller's
existing PDF DateTime 0..=9999 year clamp is kept exactly, so month/day/time of a far-future
clock survive. Pins: civil(10_000_000_000_000)=(318857,5,20,17,46,40),
civil(-10_000_000_000_000)=(-314918,8,13,6,13,20), civil(253_402_300_800)=(10000,1,1,0,0,0).
engine `cmd/mod.rs::iso_from_unix_secs` already uses the cycle fallback (last round); unchanged.
* Don't build a year-318857 SystemTime unchecked in the logging test
Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.
* 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.
- ODT export reads the tracked insertion/deletion from a Ruby inline's formatting
too (Item::marks and the paragraph's tracked check): an inserted or deleted
phonetic guide keeps its change range around the base text instead of being
written as untracked text.
- HTML export computes the paragraph-start state before the ruby's formatting tags,
so a leading space in the base text becomes instead of a space HTML can
collapse.
- docs/file-format-parity.md: restore main's format rows (the PR had replaced them
with stale copies that lost ODT keep-with-next #2813, TXT #2853 and the HTML
blockquote/MathML details) and add the ruby sentence to the retained .htm/.html
row; restore the rolled-back header change list.
- docs/localization-parity.md: restore the header change list; date the new
revision-history rows 2026-10-21.
- Tests: html_ruby_at_the_paragraph_start_keeps_a_leading_space and
an_inserted_ruby_keeps_its_tracked_insertion.
Validation: cargo fmt --all -- --check; cargo clippy -p wordcraft-formats
--all-targets -- -D warnings; cargo test -p wordcraft-formats -> 204 passed,
0 failed (doctests 0).
- Create the East Asian decoder without BOM handling: the code page is named by
\ansicpg, \cpg or \fcharset, so a GBK/Big5/UHC run that starts EF BB BF is text,
not a UTF-8 BOM that switches the rest of the document to UTF-8 (#2549 review).
- Decode raw 8-bit runs through the streaming decoder in one pass instead of one
byte at a time: ASCII body text in a Japanese/Chinese/Korean document and picture
hex skip the per-byte path, and a single \'hh byte decodes into a stack buffer
(no String allocation per byte). Picture hex no longer goes through the code page
decoder at all.
- Decode loop: decode_to_string writes only into spare capacity, so drain the stack
buffer and keep decoding until the run is consumed (the old one-call version could
truncate a run longer than the reserved capacity).
- docs/file-format-parity.md: keep main's format rows (the PR had replaced them with
stale copies that lost ODT keep-with-next #2813, TXT #2853 and the HTML blockquote
and MathML details), merge the East Asian sentence into the one RTF row — which also
removes the duplicate RTF row main carries and restores "export writes every
section" and the tracked paragraph-mark wording — restore the rolled-back header
change list, and date the new revision row 2026-10-21.
Validation: cargo fmt --all -- --check; cargo clippy -p wordcraft-formats -p wordcraft-doc
--all-targets -- -D warnings; cargo test -p wordcraft-formats -p wordcraft-doc
→ 200 + 174 passed, 0 failed (doctests 0).