* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled civil_from_days/days_from_civil copies and the
leap-year/month math around them (log timestamp, PDF /Info dates, ASN.1
UTCTime/GeneralizedTime, FormCalc Date/Num2Date, the Combine modified-time
stamp, the form date picker, and the demo-PDF timestamps) with chrono as a
workspace dependency. The engine's existing chrono (clock) is lifted to the
workspace. Every output format is unchanged and pinned by tests. CLAUDE.md now
says time goes through chrono.
Follow-up to storytold/cadcraft#1785
* Guard chrono date paths: no panic on overflow, no '+' on 5-digit years
Systemic chrono follow-ups to the UTC/calendar PR:
- logging::timestamp and combine_ui::utc_stamp dropped chrono's
From<SystemTime> (which unwraps internally and panics on a clock past
year 262143) for the fallible DateTime::from_timestamp, reading an
out-of-range clock as the epoch, as the old clamping code did.
- timestamp, utc_stamp, pdf_date and the xtask Stamp now write the year
with {:04} instead of %Y, which prefixes a '+' before years past 9999;
the old hand-rolled code wrote the plain zero-padded year.
- forms_ui calendar navigation and length, and formcalc days_in, use
checked_add for the December year successor so y + 1 can't overflow at
i32::MAX / i64::MAX (reachable from a parsed or persisted date).
Docstrings added to the chrono-touched civil_from_days and Time::from_unix
for the docstring-coverage gate. Tests cover the out-of-range-epoch and
10000-01-01 cases for each.
* 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.
* Keep FormCalc dates exact past chrono's range
MAX_DAYS accepts day numbers up to 1e8 (years past 270000), but NaiveDate
stops near ±262143, so Num2Date(99000000) and Format fell back to
1900-01-01. days_from_civil, civil_from_days and days_in now have chrono
convert within the date's 400-year Gregorian cycle and add whole cycles
back (via i128), which gives the pre-chrono results for every year. Tests
cover accepted inputs past chrono's range on both sides of the epoch,
through Num2Date and Format, with values from the old formula.
* Bump rustls-webpki and webpki-root-certs for RUSTSEC-2026-0382/0383
rustls-webpki 0.103.15 -> 0.103.16 and webpki-root-certs 1.0.9 -> 1.0.10
(both semver-compatible, lockfile only) drop the distrusted emSign
intermediates and root. Only these two entries change; cargo deny check
advisories passes. Upstream main fails the same check.
* logging/cos/sign/combine/demo: keep extended-year dates, no epoch fallback
For consistency with the stricter no-epoch-fallback rule now agreed across the sibling
chrono PRs (wordcraft#3427, filmcraft#2662): `DateTime::from_timestamp(..).unwrap_or_default()`
changes a timestamp the old code converted past chrono's year 262143 range — it collapses
to 1970-01-01 instead of the real proleptic-Gregorian date. The FormCalc Num2Date path was
already fixed (FredApps approved); this applies the same fix to every remaining conversion.
Each site uses the 400-year Gregorian cycle (146_097 days) with chrono as the only calendar
logic: chrono converts the day within one cycle, whole cycles carry in the year (i128,
saturated into i64). PDF/ASN.1 format-specific year handling is kept exactly as the old code
had it. Verified the cycle result equals the old formula across a wide range and at the extremes.
- apps/pdfcraft/src/logging.rs `timestamp`: cycle `civil_from_days(u64)`; pre-1970 epoch clamp
and ms/time-of-day format kept. Pins: +10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
(was 1970); checked_add u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch).
- crates/cos/src/writer.rs `pdf_date` (i64): cycle converter, no year clamp (old code wrote `y`
as-is, so a negative year keeps its sign). Pins: pdf_date(10_000_000_000_000)="D:3188570520174640Z",
pdf_date(-10_000_000_000_000)="D:-3149180813061320Z".
- crates/sign/src/der.rs `Time::from_unix` (i64): cycle converter; the `year` field is a u32, so
the year is cast `as u32` (wrapping) exactly as the old code did — ASN.1 year width preserved
byte-for-byte. Pins: from_unix(10e12).year=318857, from_unix(-10e12).year=4_294_652_378,
from_unix(i64::MAX).year=219_250_468, from_unix(i64::MIN).year=4_075_720_767.
- crates/ui-egui/src/combine_ui.rs `utc_stamp` (u64): cycle converter; pre-1970 epoch clamp kept.
Pins: utc_stamp(10e12)="318857-05-20 17:46 UTC", u64::MAX/2 s="292277026596-12-04 15:30 UTC".
- xtask/src/demo_pdf/pdf.rs `Stamp::from_unix` (i64): cycle converter, no year clamp. Pins:
from_unix(10e12).iso="318857-05-20T17:46:40Z"/.pdf="D:3188570520174640Z",
from_unix(-10e12).iso="-314918-08-13T06:13:20Z".
cos/sign/xtask built, tested (pdf_dates_are_utc_and_byte_identical, der::tests::times,
stamps_round_trip), fmt and clippy clean. pdfcraft-ui-egui and the pdfcraft app can't link
here (unrelated LLVM intrinsic bug in the rten-gemm dependency, 1.98.1), so their logging and
combine_ui functions + tests were copied verbatim into a scratch crate with chrono 0.4.45 and
pass there; fmt --check clean on all five packages. CI builds these on all platforms.
* 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.
Agents fixed bugs that were already reported, or already fixed on main,
because nothing told them to look first. Say to search issues and PRs
(open and closed), check whether a merged fix is on main, and build on
an open PR rather than opening a competing one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces this PR's earlier contents. It previously added a committed
`docs/plan/` tree -- a status baseline, an architecture doc and an
execution plan. Since then `main` has grown `docs/gaps.md`,
`docs/target-app-parity.md`, `docs/roadmap.md` and `docs/architecture.md`,
which are actively maintained (gaps.md was reviewed 2026-10-10 and
updated 2026-10-11) and cover the same ground better, because they sit in
the flow contributors already touch. Keeping a parallel plan would have
meant two sources of truth and a document that rots; it had already gone
conflicting twice in three days. So the plan is dropped and only the
parts with no equivalent anywhere remain.
`.github/pull_request_template.md` is new -- the repository had none,
with 167 PRs open and 66 of them conflicting. It leads with the two
things a contributor cannot be expected to know and that cannot be undone
after a merge: provenance (no Adobe asset, no committed font, no copied
GPL/AGPL code, behaviour from the spec rather than from reading another
implementation) and the never-crash rules. It also asks for a test that
has been *seen to fail* before the fix, and says plainly that per-crate
verification is preferred because CI runs the workspace anyway.
CLAUDE.md gains three things it did not record: the four-zoom review
habit (clause, function, file, project) with the tests that mirror it;
the external-PR bar; and a note that builds and tests should be scoped
per crate, because a full workspace build has locked up a development
machine and CI covers it. The commit rule now also says that `main` moves
fast enough for PRs to rot -- 396 commits in three days -- and that
conflict risk tracks how contested the files are rather than how large
the change is.
Co-Authored-By: Claude <noreply@anthropic.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, PDF-spec, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
AGENTS.md, CLAUDE.md and crates/fonts/README.md linked to storytold/craftrules, which outside contributors can't open. The links stay for team members, now labelled internal, and AGENTS.md says up front that every rule a contributor here must follow is stated in AGENTS.md and CLAUDE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shippori Mincho (and its OFL file) leaves assets/fonts/: the Japanese fonts now
live in storytold/craft-fonts. printcraft-fonts gets craft-fonts' build.rs recipe:
with CRAFT_FONTS_DIR=<checkout> it embeds the manifest's fonts as CRAFT_FONTS
(wasm32: only BIZ UDPGothic Regular); without it CRAFT_FONTS is empty and the
app builds, tests and runs as before, minus the Japanese face.
- SHIPPORI_MINCHO is now Option<&[u8]> from CRAFT_FONTS; japanese_glyph
(was shippori_glyph) uses document_japanese_font() (Shippori Mincho, then
BIZ UDMincho, then any Jpan face) and returns GlyphError::NoFont without one.
- Editing PDF text into Japanese without craft-fonts is a clear EditError that
leaves the page untouched.
- UI: every Jpan craft font (BIZ UDPGothic first) is the last fallback of
every egui family, sharing the same static bytes.
- Tests: Japanese glyph/edit tests skip without craft-fonts; new UI tests for
no-tofu Japanese with craft-fonts and working fonts without it.
- CI: the Linux test job checks out craft-fonts at a pinned SHA and runs the
tests with CRAFT_FONTS_DIR. Releases build every platform with it
(CRAFT_FONTS_REQUIRED=1) and ship each font's OFL as OFL-<family>.txt.
- ATTRIBUTION.toml gains [[build_input]] (checked by xtask assets); NOTICE,
AGENTS.md, CLAUDE.md, README and docs say fonts live in craft-fonts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release workflow gains a FreeBSD x86_64 job: packaging/freebsd/package.sh
builds in the same pinned FreeBSD VM and writes a /usr/local-style tar.gz
(app, CLI, desktop entry, MIME type, AppStream metadata, icons, licences).
README and CLAUDE.md list FreeBSD now that its CI job builds PrintCraft and
passes all 688 tests on FreeBSD 14.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ROADMAP gains an honest assessment by dimension (features, effort,
foundations, robustness, fidelity, product readiness) and by area, what
'shipped' does and doesn't mean, and a ranked list of where we're lacking
and where we're going. Numbers refreshed (804 features, 49.5% shipped,
effort ≈ 30–35%). README's stale 'What's next' becomes an honest status
summary; CLAUDE.md points every agent session at both sections.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the owner's AGENTS.md §4 / CLAUDE.md rules with what the shared standard
(craftrules/standards/never-crash.md) adds: why (lost work) and that it outranks feature work,
the `unsafe` ban (workspace `unsafe_code = "forbid"`; no unsafe exists outside vendor/),
the last-resort guard around commands and file import/export, the clippy ratchet attribute,
and a root clippy.toml that lets tests use unwrap/expect/panic/indexing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Untrusted input (PDFs, scripts, MCP calls, settings) must yield errors, not crashes: Result
over unwrap/expect, no input-derived indexing, checked arithmetic, capped allocations, bounded
recursion, lock-poison handling, and a regression test for every crash fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>