Files

18 KiB
Raw Permalink Blame History

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, VectorCraft, EffectCraft, FilmCraft, LightCraft, PdfCraft) 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; the repository is not public — outside contributors should ask a maintainer for the standards relevant to their change). Contribute reusable learnings there; shared Rust components live in storytold/craft-libs and are pinned by immutable git revision. UI libraries stay at L6. 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
ROADMAP.md One-page summary: stage, the two parity numbers, every dimension, languages, what's next
docs/target-app-parity.md The authoritative parity assessment against Photoshop 2026: numbers, weights, evidence, hours
docs/gaps.md Every known shortfall, ranked: the work list to pick from
docs/roadmap.md Milestones, current focus, what's next with estimates
docs/ui-parity.md, docs/brush-parity.md, docs/context-menu-parity.md, docs/file-format-parity.md, docs/hardware-parity.md, docs/localization-parity.md Per-dimension parity checklists
docs/parity-checklist.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/<name>/README.md (where present) Public API of that crate
photocraft-corpus + docs/development.md › Test corpora Real-file test oracles (our Photoshop-authored PSDs); with psd-tools, ag-psd and PngSuite fetched into corpus/ by cargo xtask corpus --all at the pins in xtask/src/corpus_pins.rs

2. Workspace map

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)
  tablet                     L0 standalone pen tablet input (macOS AppKit, X11 XInput2); the one isolated unsafe crate
  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 | test-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". The one exception is the isolated helper crate photocraft-tablet (crates/tablet): winit drops pen tablet data, and reading it on macOS needs an AppKit event monitor (Objective-C interop). Only its src/macos.rs allows unsafe (unsafe_code = "deny" crate-wide, every block has a SAFETY: comment, tested against real NSEvents); its X11 path and all mapping code are safe. Don't add unsafe anywhere else.
  • 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 Results.
  • 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). Never commit font files: fonts live in storytold/craft-fonts (rules: ../craftrules/standards/fonts.md). PhotoCraft uses it through the optional build input CRAFT_FONTS_DIR=<craft-fonts checkout> (read by crates/text/build.rs, exposed as photocraft_text::CRAFT_FONTS; empty without it, and everything must still work). Build with it: CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo build; details in docs/development.md › Fonts.

  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.

  1. Read ROADMAP.md, then docs/gaps.md. docs/target-app-parity.md says, dimension by dimension, where PhotoCraft is lacking; docs/gaps.md ranks the work. docs/parity-checklist.md (menu wiring) is not a measure of behaviour. When your work moves a number or closes a gap, update the parity doc and gap entry it belongs to, with the date, and bump their status line and revision history (craftrules standards/progress-docs.md).
  2. 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.
  3. docs/roadmap.md → Current focus.
  4. cargo xtask parity → docs/parity-checklist.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.
  5. 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

cargo test -p <crates you touched>
cargo clippy -p <crates> --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-checklist.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
cargo xtask test-corpus     # if you touched psd, io, codecs, compose, gpu, text or format (or: --changed decides)

While iterating, run less and keep the full commands above as the final gate: cargo test -p <crate> --lib --tests skips the examples and doctests plain cargo test builds every time (ui-egui has 11 examples), and cargo test -p <crate> --test <file> runs one integration-test file. cargo nextest run runs tests in parallel across binaries; see docs/development.md › Faster test loops.

Test corpora. Real-file corpora live in corpus/ (gitignored, never committed), fetched at pinned commits and sha256-verified by cargo xtask corpus --all: our Photoshop-authored oracles from https://github.com/storytold/photocraft-corpus plus psd-tools, ag-psd and PngSuite from their upstreams. Pins: xtask/src/corpus_pins.rs. The corpus tests are opt-in (cargo feature corpus): plain cargo test skips them, and with the feature on a missing corpus fails ("run cargo xtask corpus --all"). cargo xtask test-corpus fetches and runs them all. CI always runs them (the corpus job, cached by pin). Never commit corpus files; new oracles go to photocraft-corpus (its AGENTS.md), then a pin bump here. Details: docs/development.md › Test corpora.

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-<name>) 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/<area>_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.
  • ROADMAP.md, docs/target-app-parity.md, docs/gaps.md and the docs/*-parity.md checklists: where we stand against Photoshop.
  • docs/parity-checklist.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.

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