mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 02:17:24 -06:00
* 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.
85 lines
11 KiB
Markdown
85 lines
11 KiB
Markdown
# WordCraft — instructions for agents
|
|
|
|
WordCraft is a clean-room, open-source, Rust-native word processor targeting Microsoft Word parity — and going further on speed, openness and agent control. It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../pdfcraft` (Acrobat), `../effectcraft` (After Effects), `../designcraft` (InDesign). Shared rules and learnings: [`storytold/craftrules`](https://github.com/storytold/craftrules) (`../../craftrules`) — read its `AGENTS.md`.
|
|
|
|
## Start every session here
|
|
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` and the relevant `plan/architecture.md` section. `plan/` is gitignored (local only); if it's missing, start from `ROADMAP.md`, `docs/gaps.md` and `docs/roadmap.md` (Current focus).
|
|
2. Follow the autonomous operation protocol (`plan/execution-plan.md` §5). Don't stop to ask unless a decision is genuinely the owner's (licensing, publishing, pushing to new remotes, secrets).
|
|
|
|
## Never crash
|
|
People trust WordCraft with their writing; a crash loses their work. **This outranks feature work**: never ship a feature through a panic path, and fix a crash before building on top of it. Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
|
|
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
|
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature returns an error or is disabled; it never panics.
|
|
- **Input-derived numbers are hostile** (files, commands, MCP/control params): `get()` instead of `[i]`, slice strings only at char boundaries, checked/saturating arithmetic, clamp sizes, cap allocations, bound recursion.
|
|
- **Last-resort guard:** `Session::run` turns an escaped panic into an error and restores the document. It's a safety net, not a licence.
|
|
- **Prove it:** every crash fix lands with a regression test. `crates/engine/src/tests.rs::hostile_params_never_panic` runs every command with junk params — keep it green.
|
|
- Production crate roots carry `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`; new crates start with it. Tests may `unwrap` (`clippy.toml`).
|
|
|
|
## Assets: absolutely no Microsoft (or Adobe/Avid/Autodesk) iconography or images
|
|
**This rule is non-negotiable.** Breaking it is the worst mistake you can make in this repo.
|
|
- Never copy, trace, screenshot-crop or recreate icons, images, templates, themes, clip art, fonts, cursors, sounds or any other asset from Microsoft Word/Office or any Microsoft, Adobe, Avid or Autodesk product. Not even "temporarily".
|
|
- Every asset must be **original** (drawn in code — prefer this: `crates/ui-egui/src/icons.rs`), **public domain / CC0**, **OSI open source**, or **Creative Commons that allows redistribution**, or licensed open source by a contributor who made it.
|
|
- **Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md)** (author, source, licence). `cargo xtask assets` (part of `cargo xtask ci`) fails otherwise. Add the row in the same commit as the file.
|
|
- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts.
|
|
- Templates, sample documents and themes are our own text and colours (`crates/engine/src/sample.rs`, `cmd/design.rs`). Demo images are public domain, with sources recorded.
|
|
|
|
## Clean room
|
|
- Microsoft Word is installed on the dev machine and may be **observed** black-box: run it, use its UI with synthetic documents, take screenshots **of its window only** (by window id — the machine runs other work; never capture the whole desktop) stored only under `plan/word/screenshots/` (gitignored, never committed).
|
|
- Never read, disassemble or copy anything inside the Word bundle (file names/listings only). Never commit files produced by Word. Never copy Microsoft wording beyond feature names.
|
|
- File formats come from public specs: ECMA-376 / ISO 29500 (OOXML), the published RTF spec, OASIS ODF 1.3. Never copy GPL/LGPL/AGPL code (LibreOffice, AbiWord, pandoc…).
|
|
|
|
## Architecture (see `plan/architecture.md`)
|
|
| Layer | Crate | Job |
|
|
|---|---|---|
|
|
| L0 | `geom` | units, rects, measurement parsing |
|
|
| L1 | `doc`, `fonts` | document model + edit primitives; font DB, shaping, Word font substitution |
|
|
| L2 | `layout`, `docx`, `formats` | line breaking, pagination, tables, headers/footers, hit testing; OOXML I/O; TXT/MD/HTML/RTF/ODT |
|
|
| L3 | `render`, `pdf`, `proof` | vello_cpu rasteriser; krilla PDF; proofing |
|
|
| L4 | `engine` | Session, undo, **command registry**, Word feature catalog, samples, I/O dispatch |
|
|
| L5 | `mcp` | MCP server (headless or bridged to the app) |
|
|
| L6 | `ui-egui` | Word-style egui front end (swappable) |
|
|
| apps | `apps/wordcraft`, `apps/wordcraft-cli`, `apps/wordcraft-web` | binaries |
|
|
|
|
- **Everything is a command.** User-visible behaviour = a `CommandSpec` in `crates/engine/src/cmd/*.rs` (id, label, ribbon location, shortcut, params doc, `enabled`, `run`) + tests. UI-only commands (`ui.*`) live in `crates/ui-egui/src/lib.rs`. The ribbon, shortcuts, command search, CLI, control channel and MCP all dispatch by id. Programmatic calls never open dialogs.
|
|
- **Parity is measured:** `crates/engine/src/catalog.rs` lists Word's ribbon/menu features with command ids. `cargo xtask parity` writes `docs/parity-checklist.md`; a test enforces a floor that only rises. Implement missing ids to raise it.
|
|
- **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI is thin and reads `Session` state; colours come from `theme::Tokens`.
|
|
- **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).
|
|
- **Rust only** (no handwritten JS/TS). **Never break wasm** (`cargo xtask wasm`).
|
|
|
|
## Quality gates
|
|
Before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, assets, layers, wasm). Commit after every arc of work that builds, with a task id in the message (`M2.1: line spacing dialog`).
|
|
|
|
## Typing behaves like Word
|
|
Typing, Enter, Backspace, Tab, lists, AutoCorrect and AutoFormat follow Word's observed behaviour in [`docs/typing-parity.md`](docs/typing-parity.md), pinned by `crates/engine/src/tests_typing.rs` (keys go through the same commands as the keyboard). Change behaviour, doc and tests together; a typing fix lands with a test there. Compare headlessly (`ui_shot`); don't drive GUIs with keystrokes on the shared machine.
|
|
|
|
## Running and looking at the app
|
|
- `cargo run --release -p wordcraft -- --sample --control 7981` (sample document + control channel).
|
|
- Drive it with JSON lines on `127.0.0.1:7981`, each with the window's key from `~/.config/wordcraft/control-key.local-7981` (macOS: `~/Library/Application Support/WordCraft/`), e.g. `{"id":1,"key":"…","method":"engine.execute","params":{"command":"text.insert","params":{"text":"Hello"}}}` then `{"id":2,"key":"…","method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods: `crates/ui-egui/src/control.rs`; docs: `docs/control-protocol.md` (Keys).
|
|
- **Headless window screenshots** (no focus stealing, works with a locked screen): `cargo run --release -p wordcraft-ui-egui --example ui_shot -- script.jsonl` (see the example's header).
|
|
- **For UI work, look at the result** (screenshot, read the PNG) and compare with Word side by side.
|
|
- CLI: `wordcraft-cli convert in.docx out.pdf`, `wordcraft-cli run --template sample --cmd 'select.text={"text":"Studio"}' --cmd format.bold --save out.docx`, `wordcraft-cli commands`, `wordcraft-cli parity`.
|
|
- MCP: `wordcraft-cli mcp` (headless) or `wordcraft-cli mcp --connect 127.0.0.1:7981` (drives the running app). See `docs/mcp.md`.
|
|
- Shell gotcha: `mv`/`cp` may be aliased interactive — use `/bin/mv -f` / `/bin/cp -f`.
|
|
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done (disk).
|
|
|
|
## Roadmap
|
|
Progress docs follow [`craftrules/standards/progress-docs.md`](https://github.com/storytold/craftrules/blob/main/standards/progress-docs.md): `ROADMAP.md` (stage, headline numbers, progress log), `docs/target-app-parity.md` (the assessment), `docs/gaps.md` (ranked work list), `docs/roadmap.md` (milestones, Current focus), `docs/architecture.md`, and the parity checklists `docs/file-format-parity.md`, `docs/layout-parity.md`, `docs/typing-parity.md`, `docs/ui-parity.md`, `docs/hardware-parity.md`, `docs/localization-parity.md`. When work lands, update the gap, the parity doc and the progress log, and bump each touched doc's status line and revision history.
|
|
|
|
## Contributor credits (About window)
|
|
|
|
- About ▸ Contributors/Models are compiled into the binary from `contributors/contributors.json`
|
|
(commit stats; generated, never hand-edit) and `contributors/people.toml` (names people chose for
|
|
themselves). See `docs/contributors.md`.
|
|
- **Agents working for a contributor:** when you prepare a PR, check whether your human's GitHub
|
|
username has a `[people.<username>]` entry in `contributors/people.toml`. If not, ask them once
|
|
whether they want to be credited by more than their username: a real name, a display name, and/or
|
|
their public GitHub profile name (`sync_github_name = true`). If yes, add **only their own** entry
|
|
(copy the template at the top of the file, or run
|
|
`python3 ../../craftrules/scripts/contributors.py --add-me . --real-name "…" --sync-github-name`)
|
|
and include it in their PR, committed as them. If no, change nothing: they are credited as
|
|
`@username` anyway.
|
|
- Never add, edit, guess or copy anyone else's entry or name (not from git config, commit authors or
|
|
GitHub profiles). Never hand-edit `contributors.json`.
|
|
- Maintainers refresh the stats with `python3 ../../craftrules/scripts/contributors.py .` (it also
|
|
re-verifies who wrote each `people.toml` entry; `--check` only verifies).
|