Keep the mask when the photograph is closed

A local adjustment survived until the session ended and then did not exist.
Nothing reported it, because nothing had failed.

The sidecar format has carried masks since they were added, `Version::apply`
restores them into the graph, and `Version::update` captures them — that work
landed complete and was never called. The autosave writes `copy_settings()`,
which is a `Preset`: a map from (operation, parameter) to a number. A mask is
not a parameter. It is a rule about *where*, with a chain of its own, so it
fell outside the only thing being written, and the read path had nothing to
read.

The save now carries the stack beside the preset, on both the local and the
remote path. Deliberately not merged but replaced wholesale: this is the stack
as it stands, so a layer the user deleted has to leave the file too. Merging
two devices' stacks is `Sidecar::merge`'s job and belongs to sync (FR-NC-9).

A paste still carries no masks, and the `Option` is how that is said. Settings
travel between photographs; a mask does not, because it is drawn against one
frame and describes nothing on another — and `Scope` cannot express that,
since it filters parameters and a mask is not one.

The test fails against the old save path with "the mask must come back with
the photograph", which is the whole of the defect: not a crash, not an error,
just an edit that was not there in the morning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 12:29:41 +02:00
co-authored by Claude Opus 5
parent f4c7afb74c
commit fbf286d504
4 changed files with 141 additions and 11 deletions
+11
View File
@@ -2233,6 +2233,17 @@ impl DevelopSession {
/// Captured at full scope — framing included — because the decision about
/// what travels is made when the preset is *applied*. Copying, then
/// changing one's mind about the crop, must not mean copying again.
/// TRACES: FR-DEV-3 | FR-CAT-8
/// The local adjustment stack, for writing this image's edit back.
///
/// Beside `copy_settings` rather than part of it: that returns a `Preset`,
/// which travels *between* photographs, and a mask must not — it is drawn
/// against one frame and describes nothing on another. The save path takes
/// both; the paste path takes only the preset.
pub fn masks(&self) -> &dr_pipeline::mask::MaskStack {
self.graph.masks()
}
pub fn copy_settings(&self) -> Preset {
Preset::capture(&self.graph)
}