Commit Graph
3164 Commits
Author SHA1 Message Date
arata1972andCodex 509d5843dd M16: integrate expanded Japanese UI with upstream language settings
Co-authored-by: Codex <noreply@openai.com>
2026-10-06 06:20:23 +09:00
Brandon ThomasandClaude Opus 5.5 771b0b98d7 Issue #106: Convert to DNG and smart previews: verified, atomic writes
- Convert to DNG / Copy as DNG: the encoded DNG must decode to the same raw
  data, then goes through safe_file::write_new_unique (temp file, sync,
  read-back, hard-linked to the first free <stem>.dng / -2 / -3 name, never
  replacing a file). On failure no DNG is left, the photo is not relinked
  and the raw copy is kept.
- Photo Merge writes its DNG the same way.
- Smart previews are written atomically; smart::is_valid (header + complete
  JPEG) decides "built", has_smart_preview and the offline fallback, so a
  proxy cut short is rebuilt (reported as `repaired`); moving proxies to a
  new folder never deletes a good one in favour of a damaged copy and
  copies across drives atomically.

Stacked on the #93 branch (shared lightcraft_catalog::safe_file helper).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:17:41 -04:00
Brandon ThomasandClaude Opus 5.5 f4df914731 Issue #92: Rename Photos copies a stem sidecar another file still shares
Renaming IMG_1.CR3 alone took the shared IMG_1.xmp along and left
IMG_1.JPG without it. A Stem-named sidecar that a sibling photo file
still uses is now copied (independently, never linked) instead of moved;
the last file renamed takes it. A rollback removes the copy.

Stacked on fix/issue-105-rename-rollback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:12:25 -04:00
Brandon ThomasandClaude Opus 5.5 882a059fac Issue #92: XMP: merge into an existing sidecar instead of replacing it
Saving (Cmd+S or auto-write) now merges LightCraft's properties into an
existing sidecar (lightcraft_meta::merge_xmp): owned properties (rating,
label, dc/IPTC/rights fields, lc:*) are replaced or removed, capture time
and GPS only when LightCraft has a value; everything else (crs:,
xmpMM:History, unknown namespaces, packet wrapper) is kept byte for byte.
The result is re-parsed and checked before it is written (temp + fsync +
rename). A sidecar that isn't valid XMP is copied to .xmp.bak-<time>
(never overwriting) before it is replaced; one that can't be read at all
is left alone and the save fails.

Shared stems: when several catalogued files map to one stem sidecar
(IMG.CR3 + IMG.JPG), it belongs to the raw (else the first by name) and
the others write and read <file>.xmp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:08:18 -04:00
Brandon ThomasandClaude Opus 5.5 46732cb0fe Issue #93: Export: never overwrite an original; atomic writes
- 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>
2026-10-05 17:03:07 -04:00
arata1972andCodex 05f242c445 M11: estimate missing ARW colour from guarded camera-preview correspondences
Co-authored-by: Codex <noreply@openai.com>
2026-10-06 05:55:20 +09:00
Brandon Thomas 572b55a44a Merge pull request #110 from midasdf/feat/japanese-interface-and-watermarks
Add Japanese menus and vertical Japanese text watermarks
2026-10-05 16:49:18 -04:00
Brandon Thomas bd10f89d02 Merge main into PR #110 2026-10-05 16:48:57 -04:00
Brandon ThomasandClaude Opus 5.5 fb5b18843f Issue #105: Rename Photos reports failed rollbacks; Find Missing matches by content
Rollbacks in move_file, apply_rename and undo/redo no longer ignore
errors: every move that can't be undone is reported (old -> new), and the
catalog follows the files that kept their new names (an undoable partial
rename; a logged change for undo/redo), so nothing shows as missing.
A file that can't go back keeps the sidecars that moved with it.

library.findMissing: same name + size confirmed by the stored content
hash first, then renamed files by size + content hash, then a unique
name + size match; several look-alikes are skipped and reported in
'ambiguous' instead of guessed. Files already used by a photo are skipped.

Stacked on fix/issue-95-case-only-rename (same functions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:47:50 -04:00
Brandon ThomasandClaude Opus 5.5 dfbb1ad935 Issue #95: Rename Photos: case-only renames never overwrite another file
Whether an existing target is the photo itself is decided by file identity
(dev+inode on Unix; an exact directory entry check elsewhere), not by a
lower-case name comparison, so on case-sensitive volumes img_1.JPG next to
IMG_1.JPG is a collision (-1 suffix). Moves use a no-replace rename (hard
link + unlink where supported) and case changes go through a unique temp
name. Tests model case-sensitive and case-insensitive volumes and check
the real disk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:36:35 -04:00
arata1972andCodex 87e0181690 M16: preserve upstream defaults with an opt-in Japanese local build
Co-authored-by: Codex <noreply@openai.com>
2026-10-06 05:36:19 +09:00
Brandon ThomasandClaude Opus 5.5 0fd0ffb783 Issue #98: thumbnail cache only lists, counts and deletes its own files
scan() now accepts only <2 hex>/<32 hex>.jpg (matching shard) and the
cache's own <32 hex>.tmp<id> temp files, so prune/clear never delete and
size() never counts foreign files in a shared thumbs/ folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:23:32 -04:00
Brandon Thomas e99aa61fc4 Merge pull request #111 from storybored379/fix/unicode-timezone
Metadata: gracefully ignore invalid Unicode timezone offsets
2026-10-05 16:22:21 -04:00
Brandon ThomasandClaude Opus 5.5 58626b982d Fonts: replace Shippori Mincho with BIZ UDMincho (no Adobe-derived glyphs)
Shippori Mincho's name table credits 'FONTDASU / Google Inc. / but / Adobe' — its kanji
come from Adobe's Source Han Serif, which our asset rules exclude even under the OFL
(CLAUDE.md: no Source Han fonts). BIZ UDMincho (Morisawa Inc., design TypeBank Co., Ltd.;
OFL-1.1, from google/fonts at 63833b7) is a Mincho of its own with no Adobe glyphs, and
smaller (6.2 MB vs 8.7 MB). Attribution, licence text and NOTICE updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:19:38 -04:00
arata1972andCodex 7c799a9acf M16: add Japanese UI localization and isolated local macOS packaging
Co-authored-by: Codex <noreply@openai.com>
2026-10-06 05:15:04 +09:00
midasdf e00025aaf6 i18n: add Japanese menus and vertical Japanese watermarks 2026-10-06 01:07:26 +09:00
Brandon Thomas 294012742e Merge pull request #108 from storytold/pr/freebsd-ci-sync
FreeBSD CI: match the shared template
2026-10-05 10:19:07 -04:00
Brandon ThomasandClaude Opus 5.5 4d13061580 FreeBSD CI: match the shared template (git + safe.directory, 4 vCPUs, pinned vmactions/freebsd-vm)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:45:24 -04:00
Brandon Thomas 63380829b1 Merge pull request #91 from storytold/pr/freebsd
FreeBSD: build and test the workspace in a FreeBSD VM
2026-10-05 09:30:12 -04:00
Brandon ThomasandClaude Opus 5.5 62e16d7adc FreeBSD CI: 4 vCPUs, git in the VM, pinned vmactions/freebsd-vm
The headless UI snapshot test settles for at most 120 s; on a 2-vCPU VM
under the full test load it timed out mid-render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:58:05 -04:00
Brandon ThomasandClaude Opus 5.5 d47e941a0c docs: point agents to the shared test corpora (#90)
Real-file test oracles live in storytold/photocraft-corpus, explained in
craftrules standards/test-corpora.md. Never commit large binary fixtures
here; fetch them pinned and sha256-verified.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:24:23 -04:00
Brandon ThomasandClaude Opus 5.5 83fb206a29 FreeBSD: build and test the workspace in a FreeBSD VM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:23:26 -04:00
Brandon Thomas 3567980442 Merge pull request #89 from storytold/chore/ignore-target-dirs
Ignore /target-*/ build dirs
2026-10-05 07:38:00 -04:00
Brandon ThomasandClaude Opus 5.5 8cd791100b Ignore /target-*/ build dirs
Agents sometimes build into target-<name>/ next to target/; ignore those too so a
'git add -A' can never commit build artifacts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:37:49 -04:00
Brandon Thomas ff1e51e4d4 Merge pull request #88 from storytold/fix/issue-85-cr2-cfa
Issue #85: Raw: CR2 colour pattern from the file's CR2CFAPattern tag, not a constant
2026-10-05 07:37:19 -04:00
Brandon Thomas c05f2de251 Merge main into fix/issue-85-cr2-cfa
# Conflicts:
#	docs/parity.md
2026-10-05 07:37:00 -04:00
Brandon ThomasandClaude Opus 5.5 e87ee8e36d Issue #85: Raw: CR2 colour pattern from the file's CR2CFAPattern tag, not a constant
The Bayer layout of Canon CR2 files differs by model (GBRG on the 7D, 50D,
60D, 550D, 600D, 1200D, 1300D, 5D Mark II, 1D Mark IV; RGGB on the 40D,
5D Mark III, 6D, 5DS R and others). Read it from raw IFD tag 0xc5e0
(CR2CFAPattern: 1 RGGB, 2 BGGR, 3 GBRG, 4 GRBG), anchored at the full
decoded sensor; fall back to RGGB when the tag is missing. No model table.

Checked on 35 decoded CC0 raw.pixls.us CR2s (35 models): the tag agrees
with the mosaic's green-diagonal statistic on every file; the SensorInfo
border parity does not predict the layout. Corpus manifest gains the
reporter's samples; a corpus test asserts each layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:24:26 -04:00
Brandon Thomas efef3a5df1 Merge pull request #87 from storytold/docs/roadmap-assessment
Docs: honest parity assessment — where we stand, where we're going
2026-10-05 06:49:16 -04:00
Brandon ThomasandClaude Opus 5.5 b50fbf1285 Docs: honest parity assessment — where we stand, where we're going
ROADMAP.md: new 'Where we stand' (by dimension and by kind of user; checklist 79% vs
~60-70% as a day-to-day Lightroom replacement) and 'Where we're going' (ordered priorities:
camera colour calibration, CR3 and compressed raws, verified camera coverage, render fidelity
suite, lens profiles, AI model strategy, then HDR / Classic modules / video). Current
parity numbers, M11 status, risks (raw-source decision of 2026-10-05), log.

docs/parity.md: new rows LR-PROF-CAMERACOLOR, LR-IMP-CAMERA-COVERAGE (P0) and
LR-BEHAV-RENDER-FIDELITY (P1) so quality gaps are tracked; Top gaps reordered by user impact.

README.md: honest status summary above the feature table; stale rows fixed (compressed NEF,
DNG/original export, image watermark, Move import), camera colour and AI/HDR/video rows.

AGENTS.md / CLAUDE.md: agents read the assessment before picking work, and downgrade
✅ rows they find wrong or incomplete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:48:54 -04:00
Brandon Thomas 92bd3aa688 Merge pull request #86 from storytold/fix/issue-10-nef-compressed
Issue #10: Raw: Nikon compressed NEF (lossless + lossy)
2026-10-05 06:08:28 -04:00
Brandon ThomasandClaude Opus 5.5 6592e62912 Issue #10: Raw: Nikon compressed NEF (lossless + lossy)
Decode Nikon Huffman-compressed NEF (compression 34713): lossless and lossy
(type 1 and 2) at 12 and 14 bits, with the maker-note 0x96 linearization
curve, predictor seeds and either byte order. Clean-room: 0x96 layout and
encoding-table sizes from prose write-ups (Clevy, Claff); the fixed Huffman
tables were recovered by black-box analysis of CC0 raw.pixls.us samples
(compressed vs uncompressed shots of the same scene) and verified on ~45
files from ~35 bodies. Lossy-after-split files stay preview only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:58:31 -04:00
Brandon Thomas 8532d4ea56 Merge pull request #84 from storytold/fix/issue-78-gpu-black-export
Issue #78: GPU export: respect device limits, fall back to CPU on GPU errors
2026-10-05 05:09:57 -04:00
Brandon ThomasandClaude Opus 5.5 2218ca5a4c Issue #78: GPU export: respect device limits, fall back to CPU on GPU errors
A GPU render no longer hands back an image it cannot vouch for:
- set the wgpu device-lost callback (wgpu never reports a lost device as an
  error, so a driver reset went unnoticed); readback waits with a timeout and
  checks the poll result and map callback instead of blocking forever
- each render runs in OutOfMemory/Validation/Internal error scopes; errors
  fail that render (OOM and limit failures for that render only, device
  errors for the process) and it is redone on the CPU
- every buffer is checked against the device limit when created (no
  validation error, no process-wide disable); LIGHTCRAFT_GPU_LIMITS=webgpu|<MiB>
  creates the device with lower limits to reproduce small adapters
- the output buffer is cleared and the per-pixel kernel writes alpha 255:
  any unwritten pixel (work silently dropped, the issue's all-black export)
  fails the render; an all-black result is re-rendered on the CPU
- renders submit per stage and every 16 M invocations, the per-pixel stage
  runs in ~4 MP bands: no long submission for a slow iGPU's watchdog to reset
- ui.inspect perf.gpuReason / perf.gpuFallback, app.gpu reason/lastFallback,
  System Info and Settings say why the GPU is off (e.g. llvmpipe skipped) and
  why the last render fell back

Tests: crates/gpu/tests/fallback.rs (limit, mid-render limit, validation
error, dropped work, black photo), engine export falls back to the CPU image,
banded 3000x2000 GPU/CPU equivalence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:06:24 -04:00
Brandon Thomas 14053b2e40 Merge pull request #83 from storytold/fix/local-record-eviction
Local: forget untouched browse records of folders not browsed for 30 days
2026-10-05 02:59:23 -04:00
Brandon Thomas 1f2f25356a Merge main into fix/local-record-eviction
# Conflicts:
#	crates/engine/src/cmd/library.rs
#	crates/engine/src/library.rs
#	docs/whats-new.md
2026-10-05 02:59:05 -04:00
Brandon Thomas 2ed6af155d Merge pull request #82 from storytold/fix/persist-errors
Persistence: a failed save fails the command (change kept in memory, queued, retried)
2026-10-05 02:58:49 -04:00
Brandon Thomas cd35ebdace Merge pull request #81 from storytold/pr/release-0.2.0
Release: LightCraft v0.2.0
v0.2.0
2026-10-05 02:41:21 -04:00
Brandon ThomasandClaude Opus 5.5 65e638ce8a Release: LightCraft v0.2.0
Bump the workspace version 0.1.1 -> 0.2.0 (cargo xtask version set 0.2.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:41:10 -04:00
Brandon ThomasandClaude Opus 5.5 b14cf58630 Local: forget untouched browse records of folders not browsed for 30 days
Local browse records can be most of a large catalog (85k photos, 90 %
Local: 313.5 MB snapshot). Untouched Local records of folders whose last
browse is older than library.preferences forgetLocalDays (default 30,
0 = never) are now removed from the catalog when the library opens (and by
library.forgetLocal {dryRun?, days?}), as journaled, non-undoable ops, then
compacted in the background. Files and XMP sidecars are never touched;
browsing the folder again catalogues them afresh.

Untouched = a Local file record, not deleted, not a virtual copy, whose
state still hashes to the fingerprint taken when the browse catalogued it
(any change keeps it); records from before fingerprints need every
user-editable field empty/default. Kept too: records in albums (or as
cover), stacks, sources of virtual copies, visible/selected/edited ones,
ones referenced by undo/redo, ones with a smart preview.

Per-folder last-browsed times live in the catalog (Op::SetBrowsed,
normalised with folder_key; re-stamped at most hourly). Old catalogs load
unchanged; folders without a time are stamped now, so nothing is forgotten
on the first open after the upgrade.

persist_bench (85k photos, 76.5k Local, every 7th folder recent, 1 in 50
rated): 63.8k forgotten, plan 162 ms, snapshot 313.5 MB -> 94.9 MB.
Settings -> Performance -> Local folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:26:49 -04:00
Brandon ThomasandClaude Opus 5.5 104444edcb Persistence: a failed save fails the command (change kept in memory, queued, retried)
A command whose journal append fails now returns EngineError::NotSaved
("saved in memory but not written to disk: <reason>; LightCraft will retry")
instead of OK. The change stays applied and undoable, its ops stay queued and
are written by the next save (any command, or the frame loop with a 2 s
backoff). Queries and commands that change nothing are unaffected; a failed
compaction is reported in lastError, not as a failed command (and a failed
synchronous snapshot now backs off like a background one).

Session::unsaved(), library.info unsavedOps/unsavedError, ui.inspect unsaved;
the desktop UI toasts the failure and shows a warning on the top bar's cloud
icon until a save succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:26:49 -04:00
Brandon Thomas b312319c78 Merge pull request #80 from storytold/pr/unsafe-helper-crate
Never crash: move the one unsafe FFI call into crates/sysmem
2026-10-05 02:06:26 -04:00
Brandon ThomasandClaude Opus 5.5 1ef74a787d Never crash: move the one unsafe FFI call into crates/sysmem
The macOS malloc_zone_pressure_relief call (returns freed allocator pages
after raw decodes) lived in identical copies inside the desktop app and the
CLI. It now lives in lightcraft-sysmem, an isolated L0 helper crate and the
workspace's only crate allowed unsafe: one #[allow(unsafe_code)] module, a
SAFETY comment, a safe API that returns bytes released (0 off macOS), and
tests (repeat calls, release after large frees, concurrent calls). Both apps'
alloc_release.rs now just install the safe hook, and their crate roots gain
#![forbid(unsafe_code)], so every production crate except sysmem forbids it.
AGENTS.md/CLAUDE.md name the exception, per craftrules standards/never-crash.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:06:08 -04:00
storybored379andOpenAI Codex 35e38661a7 Metadata: gracefully reject invalid Unicode timezone offsets
Co-Authored-By: OpenAI Codex <noreply@openai.com>
2026-10-04 21:11:55 -07:00
Brandon Thomas 5525ae667a Merge pull request #77 from storytold/fix/issue-29-move-import
Issue #29: Import: Move mode with rename and template folders
2026-10-04 16:15:40 -04:00
Brandon Thomas f4316dcfb8 Merge main into fix/issue-29-move-import
# Conflicts:
#	docs/parity.md
2026-10-04 16:15:25 -04:00
Brandon Thomas 116c26d8ee Merge pull request #76 from storytold/fix/issue-35-grid-frames
Issue #35: Grid: unchanged frames reuse view data and visit only nearby rows
2026-10-04 16:14:14 -04:00
Brandon Thomas 8061e0019e Merge pull request #75 from storytold/fix/issue-37-persistence
Issue #37: Catalog: compact the log on a background thread with ordered journal boundaries
2026-10-04 16:14:02 -04:00
Brandon ThomasandClaude Opus 5.5 4788f3bfff Issue #37: Catalog: compact the log on a background thread with ordered journal boundaries
At the 2,000-record threshold the UI thread serialised and fsynced the whole catalog (1-2 s
at 85k photos after streaming). Now `persist()` starts `Journal::snapshot_in_background`:

- the worker writes `catalog.snap` for op N from a clone of the catalog (photos are Arc-shared,
  so the clone is cheap) through `Store::background_writer` (a second FsStore handle; MemStore
  opt-in; none on wasm/web, which stays synchronous);
- appends go on as before (fsynced before every command returns) and are also kept in memory
  as the "tail" (records > N);
- `Journal::poll` (from `persist` and the per-frame `persist_if_dirty`) sees the worker done and
  atomically replaces the log with exactly the tail.

Crash points: before the snapshot rename -> old snapshot + whole log; between the rename and the
log rewrite -> new snapshot + whole log (records <= N skipped as stale, existing rule); after ->
new snapshot + tail. A failed snapshot keeps the log whole and retries after another
max_records records (no retry storm). At most one snapshot in flight; a synchronous snapshot
(close, Optimize Library), reopening a library and dropping the Journal first wait for it, so an
older snapshot can never replace a newer one. On-disk format unchanged.

Tests (crates/catalog/src/tests_background.rs, a gated store holds the worker mid-flight and
"crashes" by copying the files): edits during a background snapshot survive crashes at each
point and the trimmed log holds exactly seq N+1.. in order; failure loses nothing and backs
off; sync snapshot waits (no regression); drop waits; real FsStore with a leftover partial
temp file; a proptest over interleavings of edits / start / land / finish / sync snapshot /
crash. Mutations checked: trimming the log to empty, or not waiting in `snapshot`, fail them.
Engine: `threshold_compaction_runs_in_the_background` (crash copy mid-run, reopen without close).
`library.info` -> persistence gains snapshotRunning, failedSnapshots, maxBlockingMs and
lastSnapshot.{background, blockingMs}.

persist_bench, same session (load avg ~160), UI-thread time per threshold compaction:
  85k photos: synchronous 0.85-1.7 s (median 1.25 / 1.34 s) -> background: blocking median
              45 / 74 ms (clone 1.3-154 ms + log rewrite 9-23 ms); worker 1.2-2.4 s; 230-380
              single-op appends during it at median 4-6 ms (same as without)
  10k photos: synchronous 72-233 ms -> blocking median 12.5 / 14 ms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:01:28 -04:00
Brandon ThomasandClaude Opus 5.5 ae1cbbe87b Issue #37: Catalog: stream the snapshot into the temp file instead of building two strings
Journal::snapshot serialised the catalog into a String and then copied it into a second
`format!`-ed String before writing (2x the snapshot size in memory, ~620 MB at 85k photos).
It now streams `{"format":…,"seq":N,"catalog":` + serde_json::to_writer + `}\n` through a
1 MiB BufWriter into the store (new `Store::write_atomic_with`; FsStore writes the temp file
directly, then flush + sync_all + rename + dir fsync as before; other stores buffer, as before).

Durability and format are unchanged: same temp + fsync + atomic rename; a failed write leaves
the old snapshot (and now removes the temp file); the bytes are identical to the old format
(test `streamed_snapshot_keeps_the_on_disk_format`, which also loads a legacy snapshot), so
no version bump. Also tested: `failed_streamed_write_keeps_the_old_file`.

persist_bench, 85k photos (310 MB snapshot), compaction at the 2,000-record threshold,
before/after binaries alternated in the same session (load avg ~130-170), building+compacting
only (PERSIST_BENCH_NO_LOAD):
  compaction median: before 3.5 s / 4.6 s, after 1.9 s / 2.5 s
  peak RSS:          before 926 MB / 1263 MB, after 308 MB / 366 MB
  10k photos peak RSS: 149-235 MB -> 61-68 MB (times too noisy to compare)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:43:13 -04:00
Brandon ThomasandClaude Opus 5.5 db6242d2d3 Issue #37: Catalog: instrument persistence (append, snapshot stages) and whole-frame update time
- Journal::stats(): last/max append time (encode + write + sync_data), snapshot count, last
  snapshot by stage (serialise, write+sync+rename, log reset, bytes, records), slowest
  snapshot. Reported by `library.info` → `persistence`; printed per write to stderr under
  LIGHTCRAFT_PROFILE.
- UI: `perf.frame_ms` only covers layout (`LightcraftApp::ui`); `logic()` (control channel,
  shortcuts, render polling, `persist_if_dirty`) ran untimed. Add `logicMs`, `updateMs`
  (logic + layout) and `maxUpdateMs` to `ui.inspect` → perf and a "Frame update" row to
  Help ▸ System Info.
- `crates/catalog/examples/persist_bench.rs`: synthetic 1k/10k/85k-photo catalogs (90 % Local
  browse records) on FsStore: append latency, compaction at the 2,000-record threshold, load.

Baseline (release, load avg ~240, so absolute times are inflated; median of 3 compactions):
  1k photos  (3.6 MB snap): append 7 ms, compaction 31 ms (serialise 5, write+sync 12, reset 14)
  10k photos (36.5 MB):     append 15 ms, compaction 528 ms (serialise 463, write+sync 56)
  85k photos (310 MB):      append 12 ms, compaction 4.6 s (serialise 2.2-4.4 s, write+sync
                            0.25-0.8 s); peak RSS 963 MB; load 4.7 s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:43:13 -04:00