Orphaned .backgrounds/ files get tidied — a repoint trims the one it leaves behind, and every open sweeps what got away
Follow-up to d0c5461's .backgrounds/ folder: generating, pasting, or choosing solid now trims the app's own prior file in .backgrounds/ as part of the same write when it repoints or unsets background.image away from it — silent, best-effort, never blocking the gesture that triggered it. A scheduled heal at every board open sweeps whatever that trim declined or missed: any .backgrounds/ file the board's current background.image no longer names, announced with a loss-row notice in the loose-file relocation's own voice. Legacy root-level references are untouched by both paths — the tidy scopes to .backgrounds/ only, since that is the one folder the app can prove it wrote into. Removal is via FileManager.trashItem, matching the attachment-removal precedent (recoverable, never a hard delete). Supersedes applySolidBackground's earlier "facets.png survives on disk" contract for the settled case: the ruling reads "unsets" as one more shape of "repoints away from a .backgrounds/ file", so a settled solid choice now trims the generated picture it displaces, same as switching producers does. The superseded test and doc comment are updated to the new behavior; the echo-window (no-reload-yet) case is unaffected and still leaves the file in place. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -602,6 +602,27 @@ public final class BannerCenter {
|
||||
postLoss(message)
|
||||
}
|
||||
|
||||
/// **The orphaned background-image sweep** (01-storage-format.md § Validation and healing, ruled
|
||||
/// 2026-08-09: "the open-time heal posts the calm notice-family banner the loose-file relocation
|
||||
/// heal uses — state what was tidied"): a file in `.backgrounds/` that nothing on the board named
|
||||
/// any more was moved to the system Trash, and this is the row that says so.
|
||||
///
|
||||
/// **A loss row, on `postRelocatedLooseFiles`' exact reasoning**: the app removed a file on its own
|
||||
/// initiative, on a board it opened rather than on a gesture the user made — that must be said out
|
||||
/// loud, must not evaporate unread, and must not rank as an error, because nothing failed.
|
||||
///
|
||||
/// **Only the sweep posts this.** The in-flow replace-in-place trim a background apply makes on
|
||||
/// its own prior file is deliberately silent — the ruling's "temp-file posture": the app
|
||||
/// superseding its own file inside a gesture the user already sees succeed (a fresh picture lands)
|
||||
/// is not news the way a heal discovered on open is.
|
||||
///
|
||||
/// `names` are the bare filenames the sweep actually removed, in the order it removed them. A
|
||||
/// sweep that removed nothing posts nothing.
|
||||
public func postTidiedBackgroundImages(_ names: [String]) {
|
||||
guard let message = Self.tidiedBackgroundImagesMessage(for: names) else { return }
|
||||
postLoss(message)
|
||||
}
|
||||
|
||||
/// **The skip notice** (01-storage-format.md § Malformed input, ruled 2026-07-31): the decision
|
||||
/// surface offered Skip on a defect the app has no honest repair for, the user consented, the
|
||||
/// board opened without that item — "the file stays on disk untouched, tolerated-invisible like
|
||||
@@ -973,6 +994,15 @@ public final class BannerCenter {
|
||||
// failure the user did not provoke is exactly the one they have no other way to learn
|
||||
// about.
|
||||
"Couldn't move '\(filename)' into attachments"
|
||||
case let .tidyBackgroundImage(filename):
|
||||
// **The successful sweep's own verb, negated** — `postTidiedBackgroundImages`' "Removed
|
||||
// '\(filename)' …" said the other way round, `.relocateLooseFile`'s own precedent: the
|
||||
// failure reads as the same sentence's negation rather than a different event. It stays
|
||||
// in the ordinary one-shot precedence class for `.relocateLooseFile`'s reason too — this
|
||||
// is work the app started on its own, and a failure the user did not provoke is exactly
|
||||
// the one they have no other way to learn about. In practice only the sweep's own failure
|
||||
// ever reaches here; the in-flow trim swallows its own (the ruling's "temp-file posture").
|
||||
"Couldn't remove '\(filename)'"
|
||||
case .agentGuide:
|
||||
// **"the agent guide", not "CLAUDE.md"**: the file is the app's, written for agents, and
|
||||
// most users will never have opened it — a filename here would name something they have
|
||||
@@ -1284,6 +1314,24 @@ public final class BannerCenter {
|
||||
return "Repaired duplicate id — \(sole(only))"
|
||||
}
|
||||
|
||||
/// The orphaned background-image sweep's line, `migratedTombstonesMessage`'s voice: the act
|
||||
/// first, the subject after an em dash, plurals folded, a tail naming the cause — without it the
|
||||
/// sentence would read as something that happened *to* the board rather than the app's own
|
||||
/// housekeeping.
|
||||
///
|
||||
/// - **One**: "Removed 'facets 2.png' — it was no longer the board's background".
|
||||
/// - **Several**: "Removed 3 background images — they were no longer the board's background".
|
||||
///
|
||||
/// `nil` when nothing was removed — a sweep that swept nothing is not news.
|
||||
public nonisolated static func tidiedBackgroundImagesMessage(for names: [String]) -> String? {
|
||||
guard !names.isEmpty else { return nil }
|
||||
let subject = names.count == 1 ? "'\(names[0])'" : "\(names.count) background images"
|
||||
let tail = names.count == 1
|
||||
? "it was no longer the board's background"
|
||||
: "they were no longer the board's background"
|
||||
return "Removed \(subject) — \(tail)"
|
||||
}
|
||||
|
||||
/// The lossy export's line, in the relocation family's voice — the act first, the cause after an
|
||||
/// em dash, plurals folded into their counts.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user