Files
vectorcraft/CLAUDE.md
T
Dylan M. Taylor f64db69b53 Use chrono instead of hand-rolled UTC conversion (#3387)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled Howard Hinnant civil/days-from-civil copies and the
leap-year/days-in-month tables with chrono as a workspace dependency:
- apps/vectorcraft/src/logging.rs: timestamp (log line time)
- crates/doc/src/metadata.rs: civil (Unix seconds -> [y,m,d,h,mi,s]) and iso8601
- crates/pdf/src/file_info.rs: unix (date -> seconds) and in_month, used by the
  PDF/XMP date parsers (pdf_date, xmp_date)
Output formats and parsing semantics are unchanged and pinned by the existing
tests (PDF D:... and XMP dates, offsets, leap-year rejection, year clamp 0-9999,
pre-epoch clamp). chrono uses only std, so wasm stays safe. AGENTS.md/CLAUDE.md
now say time goes through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed

chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. timestamp now uses the fallible DateTime::from_timestamp
and reads such a clock (or a pre-epoch one) as the epoch instead. Years past
9999 are written with {:04} like before, because %Y would add a '+'. Tests
cover the out-of-range and 5-digit-year cases.

* 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.

* time: preserve old out-of-range date conversion (no epoch fallback)

`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any instant
past chrono's year range to 1970-01-01. Restore the old conversion domain with
a cycle-based helper that routes the calendar through chrono (the calendar
repeats every 400 years = 146_097 days, so month/day/time come from chrono and
only the year is carried in i128, keeping every i64 exact).

- apps/vectorcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
  pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/doc/src/metadata.rs: `civil` cycle-based, keeping its clamp EXACTLY
  as the old code had it: the input is clamped to 0000-01-01..9999-12-31 first
  and the year is clamped to 0..=9999 afterwards. (With the input clamp the old
  `unwrap_or_default()` was already unreachable; this drops it and makes the
  conversion explicit, with identical output.)

Pinned old outputs in tests:
  logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
          10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
  metadata civil(i64::MIN)[0]=0, civil(i64::MAX)[0]=9999;
          iso8601(i64::MIN)=0000-01-01T00:00:00Z,
          iso8601(i64::MAX)=9999-12-31T23:59:59Z (year clamp, as before)

* 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.
2026-10-21 19:36:32 -04:00

12 KiB
Raw Blame History

VectorCraft — instructions for agents

VectorCraft is a clean-room, open-source, Rust-native vector illustration app targeting Adobe Illustrator parity (and beyond). It runs natively on macOS, Windows and Linux, and on the web via WASM. Siblings: ../photocraft (Photoshop-class) and ../pdfcraft (Acrobat-class); same conventions.

Formerly DrawCraft (renamed 2026-10-01): old .drawcraft files and "format": "drawcraft" headers still open (vectorcraft_format::LEGACY_EXTENSION), and preferences migrate from the old config folder. Keep those paths working; use the new name everywhere else.

Start every session here

  1. Read ROADMAP.md (stage, numbers by dimension, what's next) and the ranked work list in docs/gaps.md. Unless the user gives you a task, pick work from that list.
  2. Read plan/STATUS.md (local session notes, may lag the ROADMAP), then the task in plan/execution-plan.md §3 and the relevant plan/architecture.md section. Behaviour reference: public documentation only (see Clean-room below); plan/illustrator/*.md holds notes from it.
  3. Follow the autonomous operation protocol (plan/execution-plan.md §7): orient → plan → implement + test → verify → record → commit. Don't stop to ask unless §7 lists the decision as the user's.

plan/ is gitignored (local only).

Non-negotiables

  • Clean-room: never use Adobe software (absolute rule, from the project owner). Adobe's Terms of Use forbid reverse engineering, including observing a program's inputs and outputs to recreate it. So:
    • Never launch, run, script, automate (COM, AppleScript, ExtendScript, UI automation), screenshot or measure Illustrator or any other Adobe application, and never compare our output against one. Don't make test files with them, not even "synthetic" ones.
    • Never read, disassemble or copy anything inside an Adobe install (binaries, plug-ins, presets, procsets, resources, help files, fonts, ICC profiles).
    • Never copy Adobe-authored code or text into this repo, tests included: no PostScript procsets or prologs (AGM, CoolType, the Illustrator prologs), no sample files. Write test inputs from scratch from the public specifications (PostScript Language Reference, PDF / ISO 32000, TIFF, EPSF/DCS).
    • Behaviour comes only from public documentation (helpx, published specs, public forum posts), cited by URL, and from files users send us that they own (used locally, never committed). Never copy Adobe icons, artwork, presets or wording beyond feature names.
    • If a task seems to need any of this, stop and say so instead. cargo xtask cleanroom (run by cargo xtask ci) fails on Adobe copyright notices and on fixtures made with Adobe applications.
    • Never copy GPL/AGPL code (Inkscape, lib2geom…).
  • Assets: no Adobe iconography or images — ever (absolute rule, from the project owner).
    • Never add, copy, trace, redraw-from, embed or ship any icon, image, artwork, cursor, preset, swatch/brush/symbol/pattern/style library, ICC profile or screenshot from Adobe products or from any other source whose licence doesn't allow it.
    • Every image, icon, font or other asset must be one of:
      • original work created for VectorCraft by a contributor (who licenses it MIT OR Apache-2.0);
      • OSI open source;
      • public domain / CC0;
      • Creative Commons with redistribution allowed.
    • Every asset file must have a row in ASSETS.md (path, author, source URL, licence, notes). Put licence texts next to the assets (e.g. assets/fonts/OFL-*.txt) and summarize them in NOTICE.
    • cargo xtask assets (run by cargo xtask ci) fails on any unattributed asset.
    • Fonts live in storytold/craft-fonts, never in this repo (rules: craftrules standards/fonts.md). Never commit a font file here; add new fonts to craft-fonts. The app uses it only through the optional CRAFT_FONTS_DIR build option (CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo xtask ci; use an absolute path), read by crates/text/build.rs into vectorcraft_text::CRAFT_FONTS; never add it to a Cargo.toml. Everything must build, test and run with CRAFT_FONTS empty, and tests of its glyphs skip when it is. Details: docs/development.md › Fonts.
    • Code and original assets are MIT OR Apache-2.0 (LICENSE-MIT, LICENSE-APACHE, NOTICE). The ArtCraft logos in docs/brand/ are trademarks, not open source, under docs/brand/LICENSE-brand.txt: never modify them or use them outside VectorCraft.
    • Prefer art generated in code for defaults (swatches, brushes, symbols, patterns, cursors). If the provenance of an asset is unclear, don't add it.
    • Reference screenshots of Illustrator stay local under the gitignored plan/ and are never committed, published or used as assets.
  • Never crash: no panics in shipped code. A crash loses the user's unsaved work. Files, pasted data, MCP and control messages, and command params are all untrusted input.
    • Fallible work returns Result (or Option); callers handle it or propagate it with ?. Failures reach the user as an error message (EngineError for commands, the crate's own error type or String below the engine), never as an abort.
    • Workspace clippy lints ban these outside tests, so cargo xtask ci fails on them: unwrap(), expect(), panic!, unreachable!, todo!, unimplemented!. Use instead: ?, .ok_or(…)?, let … else { return … }, if let, unwrap_or / unwrap_or_default / unwrap_or_else, .first() / .last() / .split_last(), and a real error for a match arm you believe is impossible.
    • Don't panic implicitly either: use .get(i) instead of v[i] or &s[a..b] when the index comes from data. Don't divide integers by something that can be zero, cap sizes and counts read from input (allocations, images, loops), and check that floats are finite before casting them or looping on them.
    • Don't silence errors just to satisfy the lint: use let _ = or .ok() only where ignoring the failure is the right behaviour, with a comment saying why.
    • Tests may unwrap and panic (#[test], #[cfg(test)], tests/, testkit). Integration-test files start with an #![allow(…)] for that.
    • vectorcraft_engine::guard is the last line of defence. Every entry point (commands, tool events, MCP requests, the UI frame, control requests) catches a panic, rolls the document back and reports an internal error. It exists for bugs and doesn't replace Result: wasm aborts on panic, so the web app has no safety net.
    • Untrusted input is fuzzed (engine/tests/import_fuzz.rs, engine/tests/command_sweep.rs, format/tests/prop_format.rs, mcp/tests/protocol_props.rs). Extend these when you add an importer, a parser or a protocol method.
  • Everything is a command. User-visible behaviour = a command in crates/engine/src/cmd/* (id, label, menu path, shortcut, params doc, enabled, run) + tests. Tools emit commands (Begin/Preview/Commit). UI-only commands live in crates/ui-egui/src/menus.rs (UI_COMMANDS). The control channel and MCP reach all of them.
  • Adding a cursor. The app decides which cursors exist; whoever draws them works from the list. For a new tool state with a cursor of its own: add the Cursor variant (crates/tools/src/lib.rs, with a doc comment), return it from the tool's cursor() with a tool test, cover it in cursor_icon (crates/ui-egui/src/canvas.rs), draw it in crates/ui-egui/src/cursors.rs or give it a stand-in in Cursor::stand_in() until it's drawn, and add its [[cursor]] entry to crates/tools/src/cursors.toml (when it shows, in plain behaviour terms, and its hotspot; never another app's look). A missing entry only warns (cargo xtask cursors): the cursor still works.
  • Layering is enforced by cargo xtask layers. Nothing below L6 depends on egui/eframe/winit/rfd.
  • The UI is thin: panels read engine state and act through app.run(id, params). Colours come from theme::Tokens.
  • Rust only (no handwritten JS/TS). Never break wasm (cargo xtask wasm).
  • 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).
  • Shared test corpora. Real-file test oracles (Photoshop-authored PSDs, etc.) live in storytold/photocraft-corpus, explained in craftrules standards/test-corpora.md. Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified, as PhotoCraft does with cargo xtask corpus.
  • Quality gates before every commit: cargo xtask ci (fmt, clippy -D warnings including the no-panic lints, tests, layers, wasm). One task id per commit (M2.1: pen tool).

Running and looking at the app

  • cargo run --release -p vectorcraft -- --control 7979 [file.svg|file.vectorcraft] (sibling apps' agents use the same default port: if the log says it failed to bind, pick another port — otherwise your requests reach a different app).
  • Drive it: JSON lines on 127.0.0.1:7979, e.g. {"id":1,"method":"engine.execute","params":{"command":"shape.rectangle","params":{"x":10,"y":10,"width":100,"height":50}}} then {"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}. Methods: crates/ui-egui/src/control.rs.
  • For UI work, look at the result (take ui.screenshot, read the PNG) and compare with the public documentation (plan/illustrator/02-ui-ux.md notes). ui.screenshot needs a presented frame: if it errors (screen locked), check the art with ui.render / vectorcraft-cli run … --export x.png, and cover panels with a headless egui frame test (see panels/transparency.rs tests).
  • Performance: vectorcraft-cli perf checks the budgets (render/pan/zoom, hit test, save/open, SVG, Pathfinder) on a synthetic 50k-path document; vectorcraft-cli bench FILE times one file. Run them before and after renderer, format or geometry changes, on an idle machine (the report warns when the load average makes timings noise).
  • MCP: vectorcraft-cli mcp (see docs/mcp.md).
  • Shell gotcha: mv/cp are aliased interactive here — use /bin/mv -f / /bin/cp -f.
  • Parallel agents: separate CARGO_TARGET_DIR per agent; edit only the crates you own; write manifests atomically. Each target dir grows to ~30 GB: delete yours when you finish (a full disk fails links with errno=28).

Roadmap and progress docs

The progress docs follow craftrules' standards/progress-docs.md:

When a task lands, update in the same PR: the area row in docs/target-app-parity.md (score, missing items, hours) and its evidence list, the gap entry in docs/gaps.md if it closed or shrank, the checklist it belongs to, the milestone row in docs/roadmap.md, and the ROADMAP's tables and progress log when a headline number moves. Update each touched doc's status line and revision history.

  • Grade by behaviour against the public documentation (plan/illustrator/ notes), not by whether a menu item exists. Scores are self-assessed, so err low; mark each number measured or estimated.
  • Keep the README's Status section in step with the ROADMAP headline.