# AGENTS.md: guide for AI agents and contributors PhotoCraft is an open-source, native, Photoshop-comparable image editor written in **Rust only** (no JavaScript or TypeScript). **No Tauri, Electron or webview shells:** the desktop app is native egui/eframe on wgpu, and the web build is the same Rust compiled to WebAssembly (trunk + wasm-bindgen). Never add Tauri (or any webview/JS UI framework) as a dependency, build step or packaging target. The product name is always written **PhotoCraft** (`{Function}Craft` in PascalCase, like its siblings ArtCraft, ArtCraftX, DesignCraft, DrawCraft, EffectCraft, FilmCraft, LightCraft, PrintCraft) in user-facing text: UI, window titles, About, installers, release names, docs prose. Machine names stay lowercase: crates (`photocraft-*`), binaries, file names, ids (`ai.storyteller.photocraft`). Standards and learnings shared across the crafting apps live in `../craftrules` (read its `README.md`). Contribute reusable learnings there, never code; repos don't share code. The goal is 1:1 Photoshop parity (same menus, shortcuts, behaviour and file fidelity) with better performance, and every feature drivable by agents. Read this file first, then `docs/`. ## 1. Orientation (5 minutes) | Read | Why | |---|---| | `docs/architecture.md` | Crate map, dependency layers, the engine/UI seam, document model | | `docs/development.md` | Build, test, run, drive the app programmatically, debug tricks | | `docs/contributing.md` | Rules: clean-room, tests, layering, style, commits; the "add a command" checklist | | `docs/control-protocol.md` | JSON control channel: how agents drive and screenshot the running app | | `docs/ui-design.md` | Design tokens, themes, widgets, and how to match Photoshop's look | | `docs/roadmap.md` | Honest parity assessment (where we're lacking, where we're going), milestones, **current focus** | | `docs/parity.md` | Generated list of every Photoshop menu item, live or missing | | `docs/scorecard.md` | Generated scorecard: performance budgets and numbers, corpus floors, per-area checklists (tools, files, UI, type, automation, reliability, distribution), settings that do nothing | | `crates//README.md` (where present) | Public API of that crate | ## 2. Workspace map ```text crates/ geom cms color raster L0 foundation (geometry, ICC colour management, pixel formats + blend math, COW tiles) psd codecs L0 standalone format crates (no workspace deps; publishable) doc L1 document model (layers, masks, adjustments, effects, smart objects: pure data) ops paint algo text vector L2 history, brush engine, imaging algorithms, type engine, paths/shapes compose gpu format L3 CPU compositor (the oracle), wgpu compositor, .pcraft native format io plugins L4 document <-> PSD / flat formats; sandboxed WebAssembly plug-ins engine L5 Session + command registry (every action is a command) ui-egui automation L6 egui shell (thin: all actions go through the engine); MCP server testkit test helpers apps/ photocraft desktop app (eframe/wgpu), TCP control server photocraft-cli headless CLI (convert/info/run/batch/commands/mcp) photocraft-web the same app in the browser (trunk + wasm-bindgen) xtask/ cargo xtask layers | wasm | ci | stats | corpus | parity | perf | scorecard ``` **Layering is enforced** by `cargo xtask layers`. A crate may depend only on lower layers. `psd`, `codecs` and `cms` depend on nothing in the workspace. Nothing below `ui-egui` may use egui, eframe, winit or rfd. A new crate must be registered in `xtask/src/layers.rs`. ## 3. Golden rules ### Never crash (outranks feature work) People trust PhotoCraft with their work, and a crash loses it. A malformed file, a bad command or MCP param, a corrupt settings file, an odd keystroke or a full disk must produce an error the user or agent can act on, never a panic. Don't ship a feature by adding a panic path; fix a crash before building on top of it. The shared standard is `../craftrules/standards/never-crash.md`. - **Non-test code never panics.** No `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!` or `unimplemented!`. Return the crate's error type and propagate with `?`; use `ok_or(..)?`, `let .. else { return Err(..) }`, `if let`, or `unwrap_or*` where a fallback is truly correct (never one that silently corrupts a document). Unfinished features return an "unsupported" error. The only exception is a provably infallible literal: `#[allow(clippy::expect_used)]` plus `.expect("why it can't fail")`. - **No `unsafe`.** The workspace sets `unsafe_code = "forbid"`. - **Input-derived numbers are hostile.** Use `get()` rather than `[i]`/`[a..b]` for indices from files, params, selections or arithmetic on them; slice strings only at char boundaries; use `checked_*`/`saturating_*` for lengths, offsets and counts; guard division by zero and NaN/inf casts; cap allocations sized by input. - **Bound recursion** with depth limits or seen-sets (documents can be deep or cyclic). - **Don't cascade.** Handle lock poisoning (`lock().unwrap_or_else(PoisonError::into_inner)`) and treat thread joins as `Result`s. - **Last-resort guard.** The app shell must catch an escaped panic around command dispatch and file import/export, reports it as an error and keeps the document. It's a safety net, not a licence to panic. Keep `panic = "unwind"`. - **Prove it.** Every crash fix comes with a small synthetic regression test that panicked before the fix. - **Enforced by clippy.** `clippy.toml` allows `unwrap`/`expect`/`panic`/indexing in tests only. Clean crates carry `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`; new crates start with it. 1. **Everything is a command.** New user-visible behaviour = a command in the engine (`crates/engine/src/*_cmds.rs`, registered in `commands.rs`) with id, label, menu path, shortcut, params doc, `enabled` and `run`, plus tests. The UI, CLI, control channel and MCP all dispatch commands by id. Use the **exact id from `crates/ui-egui/src/menu_catalog.rs`** and the menu item goes live automatically. Only pure view/window state (zoom, panels, screen mode) belongs to the shell (`menus.rs` `UI_COMMANDS`). 2. **No format or colour assumptions.** Bit depth (8/16/32f) and colour model (RGB/Gray/CMYK/Lab…) are runtime data. Never introduce a `u8`-only pixel path in public APIs. Never assume sRGB: colour conversions go through `photocraft-cms` (`Transform`, `transform::cached`). Test at several depths. 3. **Clean-room.** We studied Photoshop and other proprietary editors for *behaviour and look only*. Never copy their code, shaders, profiles or assets. Implement from public specs (Adobe PSD spec, ICC, ISO 32000 blend modes, papers) and observation. Third-party assets must be permissively licensed, keep their license file next to them, and get a row in `ATTRIBUTION.md` (path, title, author, source, license) in the same change; so do original assets. The ArtCraft logos in `docs/brand/` are not open source (`docs/brand/LICENSE-brand.txt`). 4. **Tests are the gate.** Every change comes with tests. Format crates use round-trip, synthetic-generator, oracle and fuzz tests. Keep the PSD corpus results and the parity floor (`crates/ui-egui/src/parity.rs`) from regressing. 5. **The UI is thin and data-driven.** UI state lives in `ui-egui/src/state.rs` (serde), so the control channel can read and drive it. Colours and radii come from `theme::Tokens`, never hard-coded. 6. **Verify UI changes visually.** Render offscreen with `cargo run -p photocraft-ui-egui --example snapshot` (no window, no focus stealing), or launch with `--control` and take `ui.screenshot`. Look at the PNG. Demo images must be public-domain art, never personal photos. When fetching assets, never put a person's name, email or other personal details in requests (User-Agent, headers, URLs); use a generic `Photocraft-dev` User-Agent. 7. **Never break wasm.** L0–L6 must `cargo check --target wasm32-unknown-unknown` (run `cargo xtask wasm`). File-system code is `cfg(not(target_arch = "wasm32"))` or goes through the platform services. 8. **Performance is a feature.** Benchmark heavy operations on a 24–36 MP image in release. Work per tile in parallel (rayon), skip empty tiles, never scan a full surface per frame (cache per revision), and record before/after timings in the dev log. 9. **Never panic on input** (see *Never crash* above). A command's `run` closure and anything it calls must return `Err`, never panic, for *any* params or document state: validate params, check bounds before indexing or dividing, and reject absurd sizes before allocating. The `panic_hunt` integration test fuzzes every command with adversarial params and must stay green. ## 4. Picking work Priorities: important infrastructure first, then low-hanging parity, then the long tail. 0. **Read `docs/roadmap.md` → "Honest parity assessment" first.** It says, dimension by dimension, where PhotoCraft is lacking and the priority order of where we're going. `docs/parity.md` (menu wiring) is not a measure of behaviour. When your work moves a measured number (PSD oracle, round trips, workflow tests, performance), update that section with the dated figure. 1. **Check `docs/scorecard.md`** before picking work: each area's `missing` and `partial` rows, the performance scenarios that are over budget or not measurable yet, and the count of settings that do nothing. Its numbers are measured; prefer them to estimates. 2. `docs/roadmap.md` → **Current focus**. 3. `cargo xtask parity` → `docs/parity.md` lists every missing menu item, grouped by menu. Low-hanging fruit is usually a missing command whose algorithm already exists in `algo`, `paint`, `vector` or `text`. 4. `log/devlog.md` → the "Still open" bullets of recent entries. When parity rises, raise `FLOOR` in `crates/ui-egui/src/parity.rs` (never lower it). ## 5. Before you finish a task ```sh cargo test -p cargo clippy -p --all-targets -- -D warnings cargo xtask layers cargo xtask wasm # if you touched L0–L6 cargo xtask parity # if you added commands; commit the regenerated docs/parity.md cargo test -p photocraft-engine --test panic_hunt -- --ignored # if you added/changed commands: no panic on adversarial input (Rule 9) cargo xtask scorecard # if you moved a number: flip the checklist row in scorecard/*.toml, raise a # corpus floor, fix a dead preference, or meet a budget (then set enforce = true # in perf/budgets.toml); commit the regenerated docs/scorecard.md (CI checks it) cargo xtask perf --quick # if you touched a hot path; `cargo xtask perf --update-baseline` publishes a full run ``` Commands must **never panic** on bad input (Rule 9): every `run` closure and the code it calls returns `Err`, not a panic, for any params or document state. New commands come with a graceful-failure test (empty/out-of-range/wrong-type params → `Err`, not a crash). Then append a terse entry to `log/devlog.md` (what landed, numbers, what's still open). Sessions can end abruptly (crashes, context limits), so the dev log plus a green tree is how the next agent picks up. Keep the tree building at every step. ## 6. Parallel agents - Use your own target dir (`CARGO_TARGET_DIR=target/agent-`) to avoid the Cargo build lock, and edit only the files you own. Shared files (`engine/src/lib.rs`, the `v.extend(...)` list in `engine/src/commands.rs`, `ui-egui/src/menus.rs`, `state.rs`) get small, surgical edits; re-read before editing. - Put new commands in a **new module** (`engine/src/_cmds.rs` with a `specs()` function) rather than growing a shared file. - If someone else's in-progress edit breaks the build, wait and retry; don't fix their files. - Keep every `Cargo.toml` valid at all times: the `crates/*` glob means one broken manifest breaks everyone's build. **Create or rewrite manifests atomically**: write to a temp file outside `crates/`, then `mv` it into place. - Disk space: each target dir is about 10 GB. Delete `target/agent-*` dirs of finished agents. ## 7. Where things are tracked - `docs/roadmap.md`: milestones M0–M12, status and the current focus. - `docs/parity.md`: generated Photoshop menu coverage. - `docs/scorecard.md`: generated scorecard (sources: `scorecard/*.toml`, `perf/budgets.toml`, `perf/baseline.json`, corpus floors, prefs audit). - `docs/releasing.md`: cutting a release (`cargo xtask version`, the `release` branch), signing secrets, packaging scripts in `packaging/`. - `../craftrules/release/playbook.md`: how every storytold app builds signed release binaries (the canonical recipe; `docs/release-playbook.md` just points there); `docs/releasing.md` is PhotoCraft's specifics. - `plan/` (local, gitignored): research, parity plan, execution plan, estimates. - `log/` (local, gitignored): the dev log. - ## 8. Keeping the native format complete `photocraft-format` deliberately fails to compile when a `photocraft-doc` struct gains a field, so nothing is silently dropped from `.pcraft` saves. When you add a doc field, add it to `crates/format/src/manifest.rs` and `convert.rs` with `#[serde(default)]` so older files still load. If the field has a PSD equivalent, map it in `crates/io` too, and keep unknown PSD blocks verbatim.