Commit Graph
247 Commits
Author SHA1 Message Date
aiongg 727e66babe Merge track r2-t03 into next
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/ui-parity.md
#	docs/whats-new.md
2026-10-22 02:53:00 +00:00
aiongg 3300e6dad8 Merge track r2-t01 into next
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/raw/coverage.tsv
#	docs/whats-new.md
2026-10-22 02:49:52 +00:00
aiongg 70e9338085 Merge PR #1040: LR-IMP-FORMATS: decode Minolta MRW
docs/parity-checklist.md: header keeps the newer date; LR-IMP-FORMATS row keeps both sides' changes.
docs/raw-parity.md: header change note names both sides' changes.
docs/raw/coverage.tsv: merged cleanly; same rows as both sides, no duplicates (regenerated on the combined branch).
2026-10-22 02:42:05 +00:00
aiongg 91744336d0 Merge PR #2006: LR-EDIT-COLOR-TEMP: Kelvin white balance for the Nikon Z5II from built-in presets
crates/raw/src/kelvin_presets.rs: covered_models keeps the Z5II asserts (#2006) and the Canon cases (#2009).
docs/parity-checklist.md: LR-EDIT-COLOR-TEMP row keeps both sides' preset clauses.
2026-10-22 02:41:12 +00:00
aiongg 61ad2ff853 Merge PR #2054: LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched
crates/ui-egui/src/tests_grid.rs: both sides' tests

docs/whats-new.md: both entries under one 2026-10-16 heading

docs/parity-checklist.md: header and revision rows from both sides
2026-10-22 02:40:53 +00:00
aiongg 792b2517e5 Merge PR #1468: Issue #1449: Search Photos finds named people, and quotes keep a name together (steps 1 and 2) 2026-10-22 02:40:15 +00:00
aiongg 4ba72fa0eb Merge PR #2420: PERF-SMART: File Path rules compare in place instead of building strings per photo
docs/whats-new.md: both entries, newest first.
2026-10-22 02:38:00 +00:00
Dylan M. Taylor bfb23db2ca Use chrono instead of hand-rolled UTC conversion (#2457)
* 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 table with chrono as a workspace dependency:
- apps/lightcraft/src/logging.rs: timestamp (log line time)
- crates/catalog/src/dates.rs: civil + weekday
- crates/catalog/src/stacks.rs: iso_seconds (date -> seconds)
- crates/engine/src/cmd/face_recognize.rs: capture_secs (native only)
- crates/meta/src/datetime.rs: DateTime::unix_seconds and the valid() day check
Output formats and semantics are unchanged (pre-epoch clamp, day rollover that
normalize_iso relies on, leap-second tolerance, EXIF/XMP parsing). 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 as the epoch, keeping the old pre-epoch clamp.

timestamp and catalog's civil write years past 9999 with {:04} like the old
{y:04} did, 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, changing output the old hand-rolled
Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s was
318857-05-20 17:46:40, not the epoch). 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/lightcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
  pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/catalog/src/dates.rs: `civil` cycle-based (the callers stay inside
  0000-9999, but a far clock no longer resets to the epoch).

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
  civil 10_000_000_000_000 -> 318857-05-20T17:46:40,
        -10_000_000_000_000 -> -314918-08-13T06:13:20, i64 extremes

* 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:36 -04:00
Nathaniel LedfordandClaude Opus 5.5 84dbd1b15a PERF-SMART: File Path rules compare in place instead of building strings per photo
An ASCII path and rule text are compared byte by byte, folding case and
backslashes, with no allocation; other text takes the old code unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:07:51 -04:00
Nathaniel LedfordandClaude d184b3ef50 LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched
Every catalog change gave the grid a new list of photos, and it read every
photo of it again: each one's date group (a lookup, a date parse and a
string) and its cell's shape. At 180,000 photos that was a 100 ms frame for
every batch an import added.

- catalog: Catalog::date_runs is split into date_group (one photo's own
  group) and date_runs_of (the stack rule and the runs), which the grid
  uses too.
- ui: GridCache keeps what it read of each photo of the list, position by
  position. A new list that differs by the photos the catalog says an edit
  touched is read again only for those; anything else (another view,
  order or grouping, a stack) is read whole. The square grid reads no
  shapes. A 16-photo batch at 181,749 photos: 101 ms -> 7 ms.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-21 13:17:39 -04:00
aiongg 5443225c3c Merge track r1-i18nci into next 2026-10-21 14:54:41 +00:00
aiongg 6e893fdaa3 Merge PR #1967: CI: catalog's staging-link test module compiles only on Unix (fixes clippy -D warnings on Windows) 2026-10-21 14:52:38 +00:00
aiongg 7760d71655 Merge PR #2044: Reject impossible hour/minute/second in normalize_iso 2026-10-21 14:49:01 +00:00
Andrew FrahnandClaude Sonnet 5.5 d8f1b30bcc Kelvin presets: match multiword makes in the catalog's joined camera lookup
covers_camera tried only the first-space split, so NIKON CORPORATION NIKON Z5_2
stayed on the relative scale in the catalog and editor. Try every split, as
spectral::find_camera does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 10:04:25 +11:00
Andrew FrahnandClaude Sonnet 5.5 5e1bac1264 Merge upstream/main into pr/longtail-raws
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 09:58:17 +11:00
Arekatla Nishanth ChowdaryandClaude Opus 5.5 3a6e7d0b9b Merge main into feat/1449-people-search; people and quotes go through text_matches (#2099)
Review on #1468: main's #2099 split out text_matches() and lets unknown field
prefixes fall back to plain text. On top of it, text_matches() now also matches
Photo::people(); the free-text token is unquoted before text_matches(); an
unknown field's quoted value is unquoted in the fallback (`10:"30 am"` is the
text `10:30 am`); and the field split keeps the !t.starts_with('"') guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:18:49 +05:30
Brandon Thomas 592cb335cb Merge PR #1935: raw: alias Canon EOS R6 Mark II to EOS R6m2 2026-10-18 09:18:21 -04:00
Brandon Thomas d6a9a2e8f9 Merge PR #1845: Match f:/aperture:/focal: search tokens at f32 precision 2026-10-18 09:18:19 -04:00
Brandon Thomas 5c85bc3ee4 Merge PR #2149: Refuse catalog snapshots with an out-of-range seq 2026-10-18 09:18:14 -04:00
NetterSilverandClaude Haiku 5.5 601ddce6ef Match f:/aperture:/focal: search tokens at f32 precision
Found while testing crates/catalog/src/query.rs:130

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 13:49:17 +02:00
NetterSilverandClaude Haiku 5.5 6e97193b00 Reject impossible hour/minute/second in normalize_iso
Found while testing crates/catalog/src/dates.rs:58 (normalize_iso), used by crates/engine/src/cmd/manage.rs:201

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 11:36:44 +00:00
NetterSilverandClaude Haiku 5.5 cebb2be4d4 Keep the zone when shifting a capture time without seconds (#2146)
Found while testing crates/catalog/src/dates.rs:52 (shift_iso, tail = iso.trim().get(19..)); used by crates/engine/src/cmd/manage.rs:222

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-17 18:39:42 -06:00
NetterSilverandClaude Haiku 5.5 f48b2ad895 Search words with an unknown field prefix as plain text (#2099)
Found while testing crates/catalog/src/query.rs:146-177 (token_matches)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-17 18:39:08 -06:00
NetterSilverandClaude Haiku 5.5 035d572ac8 Refuse catalog snapshots with an out-of-range seq
Fixes #2130

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 13:57:37 +02:00
LochLogicandClaude Opus 5.5 180fdb3b41 CI: catalog's staging-link test module compiles only on Unix (clippy -D warnings on Windows)
`tests_staging` holds one `#[cfg(unix)]` test (#1502), so on Windows its
`use super::*` is unused and `cargo clippy --workspace --all-targets -- -D
warnings` (the clippy step of `cargo xtask ci`) fails in lightcraft-catalog.
Gate the module itself on Unix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:49:57 -06:00
Arekatla Nishanth ChowdaryandClaude Opus 5.5 597fb998dd Merge main into feat/1449-people-search
Both sides added code at the same places: main's Prepared filter struct next to
search_tokens()/unquote() in query.rs, its touched_since test next to the
people-search test in tests.rs, and its 2026-10-14 What's New sections next to
this one. All kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:47:22 +05:30
naryzach dde403946b raw: alias Canon EOS R6 Mark II to EOS R6m2
Canon EOS R6 Mark II writes its Exif model as 'Canon EOS R6 Mark II', which failed the spectral camera table lookup because the table registers the model as 'EOS R6m2'. Add the alias mapping and regression unit tests in lightcraft_raw and lightcraft_catalog.
2026-10-15 12:17:25 -07:00
Brandon Thomas e4519aa710 Merge PR #1487 (rebased)
# Conflicts:
#	docs/parity-checklist.md
2026-10-15 01:43:02 -04:00
Brandon ThomasandClaude Opus 5.5 b5733c7649 Merge PR #1487
Resolved against rb-base-36: Scanned keeps the base's rewrite flag next to magic and tag; Exif keeps the Panasonic lens fallback; locale catalogs merged (one duplicate key dropped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:42:11 -04:00
Brandon Thomas e6b2e6eefb Merge PR #1138
# Conflicts:
#	crates/catalog/src/lib.rs
#	crates/catalog/src/query.rs
#	crates/engine/src/cmd/library.rs
#	crates/ui-egui/src/tests_keywording.rs
#	docs/gaps.md
2026-10-14 23:46:16 -04:00
Brandon Thomas 87f0abbdb0 Merge PR #1547
# Conflicts:
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-14 22:33:11 -04:00
Brandon Thomas 9562532695 Merge PR #1662 2026-10-14 22:32:55 -04:00
Brandon Thomas 31f1f559dd Merge PR #1660 2026-10-14 22:32:54 -04:00
Brandon Thomas 43f9d51207 Merge PR #1656
# Conflicts:
#	crates/engine/src/library.rs
#	crates/engine/src/tests_library.rs
#	crates/ui-egui/src/menubar.rs
#	docs/parity-checklist.md
2026-10-14 22:32:53 -04:00
Brandon Thomas fdd05f4141 Merge PR #1779
# Conflicts:
#	docs/whats-new.md
2026-10-14 22:31:42 -04:00
Brandon Thomas 5242f02838 Merge PR #1741 2026-10-14 22:29:38 -04:00
Brandon Thomas a8f0f5d60b Merge PR #1715
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/target-app-parity.md
2026-10-14 22:29:33 -04:00
Brandon Thomas aa384d62f7 Merge PR #1466
# Conflicts:
#	docs/camera-preview-colour.md
2026-10-14 22:29:14 -04:00
Brandon Thomas aba04aaf5c Merge PR #1087
# Conflicts:
#	ROADMAP.md
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/raw-parity.md
#	docs/raw/coverage.tsv
#	docs/raw/evidence.tsv
#	xtask/src/main.rs
2026-10-14 21:59:31 -04:00
Michael Anderson 6c5eacc839 Merge remote-tracking branch 'upstream/main' into fix/1392-browse-forgets-deleted
# Conflicts:
#	docs/parity-checklist.md
2026-10-14 20:58:44 -05:00
Brandon Thomas 0bef63b6eb Merge PR #974
# Conflicts:
#	crates/engine/src/cmd/mod.rs
#	crates/ui-egui/locales/de-formats.json
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es-formats.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/fr-formats.json
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/ja-formats.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br-formats.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru-formats.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/uk-formats.json
#	crates/ui-egui/locales/uk.json
#	crates/ui-egui/locales/zh-hans-formats.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant-formats.json
#	crates/ui-egui/locales/zh-hant.json
#	docs/mcp.md
#	docs/parity-checklist.md
2026-10-14 21:55:01 -04:00
Michael Anderson bb9d3d4022 Catalog and downloads: Don't write through a link at a temp file's name (#1502) 2026-10-14 13:13:27 -05:00
midasmatterhorn 14d321c107 catalog: slice captured dates on char boundaries so non-ASCII dates don't panic 2026-10-14 18:20:34 +03:00
Brandon ThomasandClaude Opus 5.5 3b4279fc24 FreeBSD CI: three more flaky tests made deterministic
import_reads_files_off_the_ui_thread_and_can_be_cancelled: the probe was
a 150 ms sleep judged by a 300 ms wall-clock frame budget, which the
loaded FreeBSD QEMU VM can miss. It now waits on the sleeping-NAS gate of
tests_offline, which can also let a counted number of reads through:
frames must go on while every read is held (a watchdog records a frame
that waited), one batch is let through and joins the catalog, then
Cancel and wake: the files being read finish and nothing new starts.

compare_at_one_to_one_gets_a_sharp_window_per_pane ("both originals stay
decoded"): the media cache takes its budget from the process-wide memory
budget, which settings_dialog_by_keyboard_and_clicks sets to 1 GB for a
moment (and left there when it failed on FreeBSD). That leaves a 512 MiB
share, too small for two 24 MP originals (288 MB each), so one pane's
was evicted. compare_at now gives its session the default 768 MiB share;
LIGHTCRAFT_MEMORY_MB=1024 reproduces the failure without this.

dropping_the_lock_frees_the_library_while_another_thread_spawns_processes:
"the spawning thread never got a child off the ground" when a spawn took
longer than the 2 s of lock cycles. The cycles now go on until at least
5 children were spawned during them (at most a minute).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:36:53 -04:00
Jason Baldwin 2b766142bc Merge origin/main into focus-point (tracker header/summary/change log, whats-new entry moved under 2026-10-14) 2026-10-14 14:33:49 +02:00
Michael Anderson 697010d520 LR-IMP-LOCAL: Forget untouched records of deleted files when browsing a folder again (#1392)
Rated, flagged or edited photos stay and show as missing.
2026-10-14 07:32:43 -05:00
Nathaniel LedfordandClaude Opus 5.5 43ba500c6e PERF-EDIT: a smart album's count follows an edit, asking only the photos it touched
The left panel shows a count beside every album. Each catalog revision had
every album counted again, and a smart album's count asks every photo of
the library whether its rules hold: one pass over the library for each smart
album after each keyword, rating or slider tick (about 25 ms an album in a
library of 67,000 photos).

`AlbumTally` (`crates/catalog/src/tally.rs`) keeps the counts and, for each
smart album, which photos it holds. After a small edit each smart album asks
only the photos the edit touched (`Catalog::touched_since`); an album of
chosen photos is counted again only when a photo came, went or was deleted.
Counted from every photo as before: new rules, a change to an album, a stack
or the keyword list, an edit to more than 256 photos, another library, and
each new minute while an album asks about "the last N days".

What an album remembers is in proportion to the photos it holds: a bit a
photo id while ids are close together, a set when they are far apart.

The frame after a keyword in that library, both side panels open: about
54 ms -> 27 ms (156 ms before #1660).

Tests: 900 random edits against eight smart albums (keywords, flags,
ratings, labels, sliders, capture dates, file names, an album of chosen
photos) and an ordinary one, the followed counts compared with
`album_count` after each; a delete followed, three hundred photos counted
whole; the members' bits at the edges and for ids far apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:46:42 -04:00
Nathaniel LedfordandClaude Opus 5.5 5f1037a192 PERF-EDIT: an edit is followed from what it touched, not worked out from every photo
Everything derived from the catalog was keyed on its revision, which every
op raises. So each edit (a keyword, a rating, every tick of a slider) had
the grid's list queried and sorted again from every photo, the source
counted by a second full query, and the side panels tally the whole library
again. In a library of 67,000 photos the frame after one keyword took about
156 ms with both side panels open (an idle frame: 2 ms).

The catalog now remembers which photos its latest ops touched and what of
them they changed (`Catalog::touched_since`, `aspects_since`, `Aspects`). It
answers "not known" for an op that changes more than photos of its own (an
album, a stack, the keyword list), for changes too long ago, and for a
revision it did not make itself (a library just loaded).

- The view (`Session::visible`): when the same view is asked again after a
  small edit, the photos the edit touched are taken out of the list it had,
  asked about again, and put back at their place in the order
  (`Catalog::follow`). 37 ms -> 0.6 ms. Worked out whole as before: more
  than 256 photos touched, a change that is not photos' own, an album in its
  own order, Recently Added, Missing Photos, and a view that asks about "the
  last N days" (it changes with the clock too).
- The source's count: taken from the list when no filter is set, else
  counted without sorting (`Catalog::count`). 37 ms -> 0.
- The side panels (`Caches`): the folder tree, keyword tree, date groups,
  people's names and library counts are kept through an edit that changed
  nothing they read. A rating or a slider changes none; a keyword leaves the
  folders, dates and counts.

The frame after a keyword: about 156 ms -> 54 ms with both side panels
open, 71 ms -> 2 ms with the grid alone (`tests_view_cost.rs`, a synthetic
library, run with --ignored --nocapture).

Still paid on each edit: the count beside every album and smart album in the
left panel (about 25 ms for a smart album there), and the filter bar's lists.

Tests: 1,440 random edits of every photo-local kind across sources, sorts,
filters and the hold, each compared with the list worked out whole; 600
random edits comparing every kept tally with a fresh one; the clock; the
record itself, with its failure paths. `ui.inspect` -> `perf` reports
`viewFollowed` and `viewWorkedOut`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:46:30 -04:00
Nathaniel LedfordandClaude Opus 5.5 73b9cc3708 LR-LIB-SMARTALBUM: a view can hold its photos until it is refreshed
A view is asked again after every edit, so a photo that an edit makes stop
matching it leaves the grid at once and every photo after it moves up: a
keyword given in a smart album of photos without keywords takes the photo
away from under the cursor, and in a random order, with no landmarks, the
shift reads as a new shuffle (the seed does not change).

View > Hold View Until Refreshed (`library.holdView`, off by default, kept
with the library's preferences): a photo the view shows stays in it when an
edit makes it stop matching a smart album's rules, Picks or the filter bar.
View > Refresh View (`library.refresh`, also in the sort menu while the hold
is on), Reshuffle, another source, filter or sort, or new rules for the
smart album shown, shows what matches now.

- Photos that start matching still appear at once.
- A photo that is deleted, or taken out of the album or folder shown, still
  leaves at once: a held photo has to be where the view looks
  (`Catalog::query_keeping`, `within`).
- A source with no filter over it holds nothing and is queried as before.
- A refresh keeps the seed.
- The grid's count no longer reads "3 of 2" when a held view shows more than
  now match its source.

Each edit still asks the whole library again; that cost is not addressed
here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:41:44 -04:00
dev-null-undefinedandClaude Opus 5.5 b52762797f LR-MERGE-HEADLESS: HDR for Each Bracket Set
Photo ▸ Photo Merge ▸ HDR for Each Bracket Set (`merge.hdrBatch`) merges many brackets in one go with the last HDR
options, as Lightroom merges a selection of stacks: one HDR per stack among the photos, then per bracket among the
unstacked ones (`Catalog::bracket_sets`: photos taken less than about a second after the previous one's shutter
closed, at exposures at least ½ EV apart; bursts at one exposure and earlier merge results are left out). The app runs
the merges one after the other in the activity stack (cancelling one stops the rest; a failed set doesn't); the
command runs them inline for the CLI and MCP (`lightcraft-cli merge hdr-batch FILES…`) and reports merged and failed
sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:37:13 +02:00