# Conflicts: # CLAUDE.md # docs/gaps.md # docs/target-app-parity.md
17 KiB
LightCraft — instructions for agents
LightCraft is a clean-room, open-source, pure-Rust photo library + non-destructive raw developer targeting Adobe Lightroom parity (and beyond). Native on macOS, Windows, Linux; web via WASM. Sibling of ../pdfcraft (Acrobat), ../photocraft (Photoshop), ../vectorcraft (Illustrator) and ../filmcraft (Premiere), with the same conventions.
Start every session here
- Read
plan/STATUS.md(current milestone, next unchecked task, blockers). - Read the task in
plan/execution-plan.md§3, the relevant section ofplan/architecture.md, and the README/docs of the crate you touch. Behaviour/visual reference:plan/lightroom/(incl.10-observed-ui.md+ screenshots). - Know where we stand: read
ROADMAP.md(stage, headline numbers, every dimension) and, for method and evidence,docs/target-app-parity.md. The checklist counts features that exist; the real gaps are quality and coverage: camera colour calibration, CR3 / compressed raws, per-model verification, render fidelity against Lightroom, AI models, HDR / video / Classic modules (raw detail:docs/raw-parity.md). A ✅ row is not proof of parity — if you find a ✅ feature that is wrong or incomplete, downgrade it to 🟡 with a note. - Pick work from
docs/gaps.md(ranked by user impact;Bmarks beta blockers). The row-by-row tracker isdocs/parity-checklist.md(one row per feature, menu item and shortcut). When you land a feature, update its row(s) and the gap entry in the same commit, and the numbers inROADMAP.md/docs/target-app-parity.mdwhen a listed gap closes (bump each doc's Last updated line and revision history, per craftrulesstandards/progress-docs.md);cargo xtask parity(part ofci) checks that everycmd:/ctl:id and path the tracker cites still exists and thatdocs/gaps.mdlists each gap once, andcargo xtask parity --writerefreshes its summary. - Follow the autonomous operation protocol (
plan/execution-plan.md§7). Don't stop to ask unless §7 lists the decision as the user's.
plan/ is gitignored (local-only).
Merging agent branches: merge one branch, run cargo xtask ci, fix, commit — then the next. Two individually green
branches can still break each other (e.g. a new struct field vs. a new constructor).
Resuming after a crash / another session: check git status on main, git worktree list, and each worktree's
git log main..HEAD + git status for unmerged commits or uncommitted work before starting anything new. Commit small
and often so a crash loses minutes, not hours.
Never crash (outranks feature work)
People trust LightCraft with their photo libraries and edits; a crash loses their work. A malformed raw/JPEG/XMP, a bad
command, control or MCP argument, a corrupt catalog or settings file, 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, and fix a crash before building on top
of it. Full standard: ../craftrules/standards/never-crash.md
(storytold/craftrules).
- Non-test code never panics: no
unwrap(),expect(),panic!,unreachable!,todo!,unimplemented!, and nounsafe. Return the crate's error type throughResultand?; useok_or(..)?,let … else,if let, or a fallback (unwrap_or…) only where it can't silently corrupt a document. An unfinished feature returns an "unsupported" error or is disabled. Sole exception: a provably infallible literal, as#[allow(clippy::expect_used)].expect("why it can't fail").
unsafelives only incrates/sysmem(one FFI call,malloc_zone_pressure_relief, that returns freed allocator pages to macOS after raw decodes). Every other production crate root has#![forbid(unsafe_code)]. A new unsafe need goes in an isolated, well-tested helper crate like it:// SAFETY:on every block, a safe API, a safe fallback where possible, and a line here naming it.- Input-derived numbers are hostile:
get()instead of[i]/[a..b]for offsets from files, users, agents or arithmetic on them; checked/saturating math for lengths, offsets and counts; no division by zero, NaN/inf or negative casts tousize; cap allocations sized by input; slice strings only at char boundaries. - Bound recursion (depth limits or seen-sets: IFD chains, nested metadata, collections). Don't cascade:
lock().unwrap_or_else(PoisonError::into_inner)or an error; worker-thread joins areResults. - Last-resort guard: a panic hook plus
catch_unwindaround command dispatch and import/export turns an escaped panic into an error dialog and keeps the document. It's a safety net, not a licence; keeppanic = "unwind"on native. - Prove it: every crash fix lands with a small synthetic regression test that panicked before the fix.
- Enforced by clippy: root
clippy.tomlallows unwrap/expect/panic/indexing in tests only, and every production crate root (lib.rs, each binary'smain.rs) carries#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]. Not[workspace.lints]: those would also hit integration tests, examples and benches. New crates start with the attribute.
Non-negotiables
- Clean-room. Never read/disassemble anything inside Adobe app bundles (names/listings only). Never copy Adobe icons, presets, profiles (DCP), lens profiles (LCP), camera matrices, fonts. Observation of the installed Lightroom is read-only (it syncs the user's personal library: never import/edit/rate/delete there). Never copy GPL/LGPL/AGPL code (darktable, RawTherapee, ART, LibRaw, rawspeed, rawloader, rawler, lensfun, dcraw-derived GPL code…).
- Pure Rust in the product. No C/C++ dependencies.
- Layering (
plan/architecture.md§3, enforced bycargo xtask layers): nothing below L5 depends on egui/eframe/winit/rfd. - Everything is a command (
crates/engine): id, label, menu path, shortcut, params, enabled(), run(). UI, CLI, control channel and MCP all dispatch by id. Every slider is adevelopcontrol spec. - Long-running work shows in the activity stack (
docs/background-tasks.md): anything that runs off the UI thread and can take more than a moment holds aTaskGuardfromsession.activity.start(kind, label, cancel)for its whole run. It adopts the cancel flag the job already checks (Cancel::Flag), or usesCancel::Nowhen the job can't stop. No progress window, panel or progress toast of its own; its tests assert the row. - Resolution independence: settings use normalized image coordinates and relative radii; previews and exports must match.
- 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). - Quality gates before every commit:
cargo xtask ci --quick(fmt, clippy -D warnings, the changed packages' tests, layers, assets, and heif / parity / wasm when touched). GitHub runs the fullcargo xtask cion every PR (.github/workflows/ci.yml); run the full one locally before merging branches (below) or when--quickcan't see the risk. Oneciruns at a time per machine; others wait their turn (xtask/src/ci_lock.rs). - Commits: one task id per commit (
M2.3: local Laplacian highlights/shadows). Only green states. End messages with the attribution line required by the environment.
Assets: icons, images, fonts (ABSOLUTE RULE — never violate)
- Never use any iconography, image, artwork, font, sound or other asset from Adobe products (no Lightroom/Creative Cloud icons, no screenshots, no presets/profiles/LUTs, no UI bitmaps — not even as a temporary placeholder or "reference copy"). Observing Adobe's UI to imitate layout and behaviour is allowed; copying or tracing its assets is not.
- This includes Adobe's open-licensed assets: no Source Sans/Serif/Code or Source Han fonts, no Adobe Fonts, no Adobe-published icon sets, sample photos, colour profiles or LUTs — even when OFL/MIT. The UI font is Inter (OFL); Japanese and Chinese fonts come from craft-fonts (below). One exception (maintainer decision, 2026-10-07): Noto Sans/Serif CJK (Google-branded, OFL, co-developed with Adobe as Source Han) is allowed via craft-fonts for Chinese text, because nearly every OFL Chinese face derives from it. Use it unmodified under its OFL; this does not open the door to Source Han under Adobe's name or to any other Adobe asset.
- Every asset in the repository must be one of: our own original work (e.g. icons drawn in code as vectors, procedurally generated demo photos), public domain / CC0, Creative Commons (CC-BY / CC-BY-SA with attribution honoured), OFL (fonts), or permissive open-source (MIT/Apache-2.0/BSD/ISC) — or contributed by a person who created the asset and licenses it openly.
- Exception:
docs/brand/. The ArtCraft name, wordmark and logos there are ArtCraft Team trademarks, not open source and not covered by LightCraft's MIT OR Apache-2.0 licence (LICENSE-MIT,LICENSE-APACHE,NOTICE); their terms are indocs/brand/LICENSE-brand.txt. Use them only unmodified and never redraw, recolour or derive from them. - Every asset must have an entry in
assets/ATTRIBUTION.md(path, title, author/creator, source URL or "original work", licence, date added, modifications) and its licence text when required (e.g.assets/fonts/OFL-*.txt). Add the entry in the same commit as the asset. Assets without an attribution entry must not be committed. - Fonts live in storytold/craft-fonts, never in this repo. Don't commit
font files here (Inter, already in
assets/fonts/, is the one exception); add new fonts to craft-fonts. LightCraft uses it as the optional build inputCRAFT_FONTS_DIR:git clone https://github.com/storytold/craft-fonts ../craft-fontsthenCRAFT_FONTS_DIR=../craft-fonts cargo run -p lightcraft(or any cargo/xtask command).crates/engine/build.rsembeds the manifest's fonts aslightcraft_engine::CRAFT_FONTS(wasm32: BIZ UDPGothic Regular only);cargo xtask webships Noto Sans CJK SC Regular as a separate asset loaded by the UI before startup. The UI (theme::font_definitions) and the export watermark renderer use CJK faces (picked by script) as fallbacks after Inter; the UI also falls back on the faces of other scripts (Thai, Arabic, …). Unset,CRAFT_FONTSis empty: everything builds, tests and runs, but Chinese and Japanese text lack glyphs. Releases always build with it (release.yml,CRAFT_FONTS_REQUIRED=1) and ship the fonts' OFL licences. Tests that need these fonts skip without it; the FreeBSD CI job runs them with it. Rules: craftrulesstandards/fonts.md. - Icons drawn in code (e.g.
crates/ui-egui/src/icons.rs) are original work and are recorded inassets/ATTRIBUTION.mdas such; do not trace them from Adobe icons. - Demo/test images: generated procedurally by
lightcraft-scenes, or CC0 downloads kept in the gitignoredcorpus/with their source recorded. Screenshots of Adobe apps live only in the gitignoredplan/and are never committed or published. - Enforced:
cargo xtask assets(inci) fails when an image/icon/font/sound/video/raw/ICC/XMP file is not matched by a path pattern in the first column ofassets/ATTRIBUTION.md, when a referenced licence file is missing, when a file name suggests Adobe material, or when an Adobe profile/template format (.dcp,.lcp,.lrtemplate, …) appears. Never weaken this check to make a commit pass — fix the attribution or remove the asset. - When in doubt about an asset's licence: don't use it.
Running and looking at the app
cargo run --release -p lightcraft -- --control 7980opens the desktop app with the JSON-lines control server (seedocs/control-protocol.md).- Never send OS-level synthetic input (osascript/System Events keystrokes or clicks,
cliclick, accessibility automation): it goes to whatever window is frontmost — the user's terminal or other apps. Drive LightCraft only through its control channel (ui.key,ui.pointer,ui.clickWidget,ui.menu.invoke) or headless snapshots. - For UI work, look at the result: drive via the control channel and take
ui.screenshot, compare withplan/lightroom/screenshots/. - Unattended (display asleep/locked, CI, nightly runs): prefer headless snapshots — no window needed:
lightcraft-cli snapshot --demo --script tour.jsonl -o out.png --size 1600x1000(control-protocol requests, one per line; seedocs/control-protocol.md→ Headless rendering). In a running app useui.screenshot {"path": …, "headless": true}; windowed screenshots fall back to headless after 2 s. - MCP:
lightcraft-cli mcp(headless,--demofor the procedural library) orlightcraft-cli mcp --connect(drives the running app). Seedocs/mcp.md. Quick non-UI checks:lightcraft-cli render in.jpg -o out.jpg --set light.exposure=1. - Export goes through
lightcraft_engine::export(one encoder for app, CLI, MCP and web); UI-only commands live incrates/ui-egui/src/menus.rs. - Text boxes use
crates/ui-egui/src/text_field.rs(TextField), notegui::TextEdit: it gives every field the Cut / Copy / Paste / Select All menu, Esc restoring the text, and reports how an edit ended. Give each a widget id that is unique on screen. Older fields still onTextEditmove over as they are touched. - Shell gotcha:
mv/cpare aliased interactive here — use/bin/mv -f//bin/cp -f. - Parallel agents: separate git worktrees and
CARGO_TARGET_DIR=target/agent-<name>; each agent uses its own control port (pick one in 18000–19999, never the default 7980) and its own scratch subfolder (<scratch>/<agent-name>/) — neverrm -rfshared paths; delete your target dir when done (disk is shared); keep everyCargo.tomlvalid at all times (thecrates/*glob means one broken manifest breaks everyone). - Test corpora:
cargo xtask corpus --downloadintocorpus/(gitignored, CC0 only). Never commit media. - Shared real-file test corpora (Photoshop-authored PSDs, etc.) live in
storytold/photocraft-corpus, explained in craftrulesstandards/test-corpora.md. Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified, as PhotoCraft does withcargo xtask corpus.
Testing & performance (do this often)
- Unit/property tests next to the code; end-to-end tests drive real binaries (
crates/mcp/tests/e2e.rs,apps/lightcraft-cli/tests/). New features need at least one test that would fail without them. - Run the app after every user-visible change: launch with
--control 7980, drive it with a JSON-lines script (docs/showcase/run.py file.jsonl), takeui.screenshot, and look at it. Checkui.inspect→perf. - Benchmarks:
cargo xtask bench(24 MP raw from corpus; CPU and GPU columns) appends totarget/bench/history.jsonland flags CPU-time regressions > 20 % vs the previous run (--strictto fail). Run it before and after perf work. - Measure, don't guess:
LIGHTCRAFT_PROFILE=1prints per-stage pipeline timings to stderr; time CLI renders with/usr/bin/time. Record numbers inplan/STATUS.md→ Metrics. Budgets: slider update ≤ 16 ms (draft) / loupe ≤ 60 ms on ~2.5 MP; export ≤ 1 s per 24 MP JPEG. - Never commit media: test images are procedural (
lightcraft-scenes) or CC0 downloads in the gitignoredcorpus/. - A test that can't run here (no GPU, corpus file, craft-fonts face, model) calls
lightcraft_testkit::skip!(Gpu, "why")before it returns, not a bareeprintln!.cargo xtask cithen lists the skips and the machine's GPU adapters and data, and writestarget/ci/report.jsonandtarget/ci/summary.md(Markdown for a PR).
Map of the code
geom, color, raster, tiff (L0) → raw, codecs, meta, develop (L1) → pipeline → catalog → engine
→ ui-egui, mcp (L5) → apps lightcraft (desktop), lightcraft-cli (render/commands/MCP). scenes generates demo
photos. xtask = tooling (ci, layers, assets, parity, wasm, corpus, stats); testkit = test helpers
(dev-dependency only). flake.nix +
nix/package.nix = the Nix package (nix build builds both binaries with the craft-fonts input, installs the
desktop file/icons/AppStream metadata and runs cargo test --workspace; nix develop = dev shell). Community-maintained; CI only
evaluates it (.github/workflows/nix.yml), so the build may lag behind the workspace; see README → Quick start.