- lightcraft_catalog::safe_file: one tested helper for crash-safe writes
(temp file in the same folder, sync, rename, folder sync; temp removed on
failure), write_new/write_new_unique that never replace a file and read
the result back, same_file, and a per-thread fault injection for tests.
- engine::originals::OriginalGuard: refuses a write whose target is a
catalogued photo's original or XMP sidecar (by file identity), whatever
the conflict policy or exact path. Used by run_batch (export, Export with
Previous, control/MCP exact paths), ui.render/ui.screenshot paths, merge
previewPath, MCP render_photo/screenshot paths (new query command
export.checkTarget) and lightcraft-cli render.
- The conflict policy now covers an export's sidecars together with the
file (Unique gives both the same free name, Skip skips when either is
taken).
- export::write_file replaces the plain std::fs::write in the desktop app,
CLI, MCP and engine writers; XMP sidecar writes use the shared helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
library.import mode `move` (UI: Transfer ▸ Move, MCP import tool): reuses
Copy's destination, folder organisation (incl. custom templates) and rename
template. Each photo and its XMP sidecars (both namings) are placed without
touching the source (hard link on the same volume, else a new file that is
copied, synced and compared byte for byte; taken names get -1, -2…, never
overwritten). Sources are removed only after the catalog records are
committed and saved, and only when the destination is still intact; failed,
duplicate, unchecked and in-library files keep their sources; sources that
can't be removed are kept and reported (`kept`). Undo removes the catalog
records and leaves the moved files at the destination.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- merge output_path: a bounded search for a free name instead of
`for k in 1..` + unreachable!().
- library open: Option::insert returns the new library, no expect.
- relink: a one-op batch unwraps through <[Op; 1]>::try_from, no expect.
- lightcraft-mcp (already clean) now #![deny] unwrap/expect/panic/
unimplemented/todo/unreachable; clippy.toml as in the steering PR.
crates/engine/src/export.rs keeps one expect (open PR #51 edits that
file), so lightcraft-engine doesn't get the deny attribute yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
library.import organize now also takes a folder template, e.g.
{date:%Y}/{date:%Y%m%d} -> 2026/20260114 (Organize::Template). The
template's own / (or \) make the folder levels; each level is expanded
with the rename tokens and sanitized, so metadata can never add a level or
climb out of the destination; absolute paths, drive letters, ~ and . / ..
levels are refused. Dated by capture time, else the import time; a level
whose metadata is missing becomes 'unknown'. The import dialog's Folders
menu gains 'Custom template...' with a template field, tag help, live
validation and an example full destination for every mode; the dialog
stays open (with a toast) when the import is refused. The MCP import
helper passes mode/destination/organize/rename through (docs/mcp.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rename::expand_tokens is shared by Rename Photos, import renaming and export naming, so {seq:N},
{date:%…}, {camera}, {title}, {ext} work in exports too (a bare {seq} still pads to 3 digits there).
New shared tokens: {num} (number at the end of the name), {folder}, {lens}, {iso}, {rating},
{creator}. An export name that expands to nothing keeps the original name. The MCP export tool
passes startNumber through and documents the tokens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The crop tool fell through to crop.set with an empty object when none of
its parameters were given, so a call with a guessed parameter (e.g.
`aspect: "1:1"`) or `reset: false` answered "ok" and left the photo
unchanged. It now returns an error naming the parameters it needs, as
set_develop already does for an empty call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- library.browse {path, subfolders}: a folder's photos are read in place as "local" photos
(catalog flag, SetLocal op) that only folder views list — not All Photos, albums, date
groups, keyword counts or stats. photo.addToLibrary promotes them; importing the folder for
real promotes instead of reporting duplicates. The browsed folder persists with the view.
- UI: Local section (Pictures, Desktop, Downloads, Home, the browsed folder, Browse Folder…);
folder views get a breadcrumb header (each part opens that folder), Include subfolders and
"Add N to My Photos".
- Renderer: a job that fails (unreadable / missing file) is remembered by slot + key and not
requested again every frame — the grid re-queued it forever (a render loop per broken
file). The loupe says "This photo can't be opened" with the reason; grid cells show "!".
ui.inspect lists pending render slots.
- Headless app.export (MCP, lightcraft-cli run) exports the selection like the desktop app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Engine: export split into prepare_export (session thread, cheap) and PreparedExport::run
(Send: read/decode/render/encode); run_batch with per-photo progress, cancel and collected
errors; export_batch keeps the stop-at-first-error behaviour for scripted calls.
- Desktop: the Export dialog, Export with Preset (menu and context menu) and Export with
Previous run their batch on a worker thread (app.export {background: true}); a progress
panel shows count / file / Cancel; ui.inspect reports export {running, last}.
- Headless: settle waits for a running export; `lightcraft-cli snapshot` finishes it before
exiting. Web keeps foreground exports (downloads are main-thread).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Built-in export presets (JPEG Small 2048 px, JPEG Large full size, Original + Settings, DNG)
and user presets saved with the library (export.presets / savePreset / deletePreset);
app.export {preset, …overrides} everywhere (desktop, MCP, lightcraft-cli run).
- File → Export with Preset submenu (built-ins, user presets, Custom…); dialog: preset
picker, Save preset, Choose… folder picker, subfolder, if-file-exists (add number /
overwrite / skip), start number, TIFF compression (none / LZW / ZIP); {date} naming token.
- One export_batch for the desktop app and headless MCP (paths, conflicts, sidecars);
ExportOptions::to_json is the exact inverse of from_json.
- ⌘E = Export with Previous (alias); parity: Show in Finder row was stale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`lightcraft-cli run [--demo | --library DIR | --connect [ADDR]] [--import PATH]… CMD k=v … CMD …`
runs any engine/UI command or control-protocol method (headless, on a persistent library, or
against the running app) and prints one JSON line per command; non-zero exit on failure,
--keep-going, --script FILE|- for JSON lines. Imported photos are selected (first active) so
the chain acts on them; a library's edits are saved for later invocations. Docs: docs/mcp.md,
README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Resize modes: long edge, short edge, width, height, W x H (either orientation), megapixels,
percent; "don't enlarge" (default on); ppi written to JFIF density / PNG pHYs / TIFF tags.
- Full-size export of a cropped photo now renders the crop at native resolution (it was
upscaled to the uncropped long edge).
- Formats: Original (file copied byte for byte + XMP sidecar with the edits, named after the
output) and DNG (raw photos re-encoded losslessly, edits in the embedded XMP).
- Export dialog: Resize-to dropdown, values, Don't enlarge, Resolution; DNG/Original hide
pixel options.
- lightcraft-cli render goes through the shared exporter (colour space, metadata, ppi) and
takes any app.export option via --opt KEY=VALUE; MCP export schema lists the new params.
- xtask resolves the workspace root at run time (a moved checkout broke ci).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderRequest gains a sample format (OutputDepth: U8 for previews, U16, F32Linear); deep renders
run the per-pixel stage with the exact sRGB curve and return Rendered::deep alongside the 8-bit
image (GPU stays 8-bit: deep exports render on the CPU). Export encodes deep renders: 16-bit PNG and
TIFF, 32-bit float linear TIFF with a linear ICC profile we generate, 10-bit AVIF (YCbCr planes into
ravif). Output sharpening and watermarks work on deep images. ExportOptions.bitDepth with per-format
defaults (TIFF 16-bit, others 8-bit), export dialog bit depth choice (remembered with the last
export), MCP export schema. Tests: a smooth gradient has 1024 levels in a 16-bit TIFF vs 13 in
8-bit, float TIFF decodes to the same light as 16-bit, per-format depth rules, AVIF 10-bit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderRequest gains an output space (default sRGB; previews stay sRGB). The per-pixel stage converts
scene-linear Rec.2020 to the target primaries, gamut-maps into the target gamut and encodes with the
target curve, on CPU and GPU (WGSL finish kernel reads the matrix, luma weights and curve from its
parameter block). sRGB output is byte-identical to before on the CPU. Export embeds our own
generated ICC profile for the space; AVIF stays sRGB. ExportOptions.colorSpace, export dialog and
MCP export schema. Tests: P3 colour survives a P3 export (decoded through the embedded profile),
every space/format round-trips its profile, GPU equivalence per space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/mcp (lightcraft-mcp, L5): hand-written MCP over stdio (JSON-RPC 2.0,
protocol 2025-06-18 with 2025-03-26/2024-11-05 negotiation): initialize,
ping, tools/list, tools/call, resources/list/read. Two backends behind one
trait: Headless (in-process engine Session; answers engine.execute,
engine.commands, ui.render, app.export) and Remote (the desktop app's
loopback JSON-lines control channel; lazy connect + reconnect).
Tools: helpers (list_commands, run_command, import, query_photos,
select_photos, list_controls, get/set_develop, apply_preset, crop,
render_photo -> image content, export), UI tools when connected
(screenshot -> image content, inspect_ui, set_ui, list_widgets, click,
press_key, type_text, pointer_gesture), plus one cmd_<id> tool generated
per command in the registry (--compact omits them).
apps/lightcraft-cli: `mcp [--connect [ADDR]] [--demo] [--compact] [FILES]`,
`render <in> -o <out> [--set control=value]... [--settings] [--preset]
[--size] [--quality]`, `commands`, `controls`.
Acceptance e2e: crates/mcp/tests/e2e.rs sets exposure over MCP and renders
(headless and through the TCP transport); apps/lightcraft-cli/tests/cli.rs
does the same through the real binary's stdio. Docs: docs/mcp.md,
docs/control-protocol.md. Also rustfmt of one line in pipeline/finish.rs
(main was not fmt-clean).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>